Session logs and completion notes. Last updated: 2026-05-02 (W-0091 through W-0100)
Phase: Phase 20 — Analysis Tools and Strategic Context Active work: Complete (W-0091 through W-0100 done) Next phase: TBD
| Phase | Title | Status |
|---|---|---|
| 1 | Standardisation and Governance | Done |
| 1.5 | Data Modelling and Source Definition | Done |
| 2 | Automated Data Pipeline | Done |
| 2.5 | Discovery / Research Spikes | Done |
| 3 | Visualisation and Deployment | In progress (W-0013 manual step remaining) |
| 4 | Qualitative Data Extraction | Deferred |
| 19 | UI Layout and Information Architecture | Done |
| 20 | Analysis Tools and Strategic Context | Done |
Items completed:
-
W-0091:
config/metrics.yamlextended withcontrollabilitysection (High / Medium / Low / Hybrid for every canonical metric).config/metric_groups.yamlcreated with three groups: Management Controllable, Market-Driven, Productivity-Focused.glossary.mdupdated with controllability level definitions. -
W-0092: "Group" dropdown added to sticky control bar (after Share fieldset). Options: All Metrics, Management Controllable, Market-Driven, Productivity-Focused. Filter persisted to
localStorage["metricGroup"]. When a group other than All Metrics is active,renderCharts()skips metrics not in the group. METRIC_GROUPS const inlined inindex.html. -
W-0093: Collapsible "Golden Metrics" sparkline panel added below KPI row. Six sparklines (CET1, ROE, CTI, NIM, NPL, CFR) rendered as compact 8-quarter Chart.js lines. Panel defaults to collapsed; state persisted to localStorage. Destroyed/rebuilt on open/close to avoid canvas reuse issues.
-
W-0094: "Correlations" tab added (after productivity). Renders a Pearson correlation heatmap (diverging colour: red = -1, white = 0, teal = +1) for all selected banks and visible periods. Highly-correlated pairs (|r| > 0.85) listed below the matrix. Respects the metric group filter.
-
W-0095: Collapsible "Market Context" section added below the ranking table. Shows a user-selected metric vs OCR overlay (OCR on right axis). Metric selector populated with current tab's metrics. State persisted. Reachable via Analysis ▾ dropdown or by scrolling.
-
W-0096: "Strategy Scorecard" tab added. STRATEGY_PILLARS const (5 pillars: Customer, Efficiency, Resilience, Innovation, Returns) inlined in
index.htmlfromconfig/strategy_pillars.yaml. Scorecard renders pillar cards with sector-average value, QoQ trend arrow (coloured by good-direction), peer percentile bar, and leading/lagging badge. -
W-0097: Export CSV and Print buttons added inside Strategy Scorecard tab. CSV export generates
strategy-scorecard-{period}.csvwith bank|pillar|metric|latest_value|prior_value|trend|leading_or_lagging columns. Print button triggerswindow.print(). Print stylesheet added to hide nav/controls/tabs and show only pillar grid. -
W-0098: "Dimensionality Reduction" tab added. Vanilla-JS power-iteration PCA computes top 6 principal components from latest-period bank data. Scree plot (bar chart of % variance explained per component) and 2D scatter projection (PC1 vs PC2) rendered side-by-side. Respects metric group filter.
-
W-0099: Chart tooltips extended to append
[controllability: X, pillar: Y]context lines.docs/glossary.htmlgains controllability (colour-coded) and pillar badges on every metric term via a small JS injection at page load. -
W-0100: "Analysis ▾"
<details>/<summary>dropdown added to the site nav ondocs/index.html. Dropdown lists Correlations, Dimensionality Reduction, Strategy Scorecard, and Market Context — each activates the corresponding tab/section and scrolls into view.
Mini-retro:
- All 10 items were frontend-only changes (HTML/CSS/JS) plus config YAML files. No Python pipeline changes were required — all 218 tests pass.
- The PCA implementation uses power iteration (no external library) with deflation, which is reliable for the small p (≤30 metrics) and n (≤26 banks) sizes involved.
- W-0091 established the controlled vocabulary (controllability levels) before downstream items consumed it, preventing the ad-hoc divergence that would have occurred if each tab had its own classification.
- Root cause for duplicated badge data in glossary.html: static HTML can't directly read YAML, so the JS injection approach (inline const → badge) is the least-surprising pattern for a static site. A build step that generates JSON sidecars from YAML would be more maintainable at scale.
Items completed:
-
W-0038: Bank detail navigation wired.
docs/index.htmlbank selector pills now include a↗link tobank.html?bank=<name>. Snapshot table and ranking table entity cells include the same link.bank.html(single parametric page) was already built with all required features: KPI strip, sparklines for all RBNZ metrics, peer group selector, and disclosure metrics table. Navigation is now connected from the main dashboard. -
W-0039: Metric detail navigation wired. Chart card
<h3>titles inrenderCharts()are now<a>elements linking tometric.html?m=<slug>. AMETRIC_SLUGSconstant maps all 24 metric names to their URL slugs. A "metric detail →" link was also added to the chart nav bar and to the metric info popover footer.metric.html(single parametric page) was already built with ranking table, history chart, and definition card. -
W-0066: Peer group selector already present in
bank.html— marked done. The selector provides "Big 4", "Top 6", "All standalone", and a custom multi-select. Peer group state persists inlocalStorage["selectedBanks"]on the bank detail page. -
W-0072: Plain-language review of auto-narrative panel. Abbreviations expanded on first use: "NIM" → "Net Interest Margin (NIM)", "ROE" → "Return on Equity", "NPL Ratio" → "Non-Performing Loan Ratio". "bps" expanded to "basis points" throughout. Output sentences read at Year 10 level.
-
W-0073: AI-slop removal from user-facing prose. Em dashes removed in
docs/glossary.html,docs/methodology.html,docs/lineage.html, anddocs/index.htmlnarrative templates. Replaced with punctuation matching the grammatical role (period, semicolon, colon, comma). Metric name separators changed fromName — AbbrevtoName (Abbrev, unit)in methodology page. No passive constructions, AI vocabulary words, or Wh- sentence openers were found.
Mini-retro:
- Navigation was the missing piece between index.html and the already-built bank/metric detail pages. The parametric approach (
bank.html?bank=ANZ) satisfies the static-site requirement without creating 26 near-identical files. - W-0072 and W-0073 were prose-only changes — no Python tests needed. All 198 existing tests still pass.
- Root cause for em dashes: initial content was generated without a style constraint against them. Adding the
remove-ai-slopchecklist to the contribution guide would prevent recurrence.
Items completed:
- W-0079: Sticky control bar — bank selector + controls moved out of page-header into
position: sticky; top: 48px.sticky-controlsdiv. Page header retains only h1, subtitle, status, and freshness. - W-0080: Summary KPI row — 4-card summary section before chart tabs; sector-average CET1/ROE/CTI/NIM with best/worst bank per metric.
renderKpiRow()added, called fromrender(). - W-0081: Labelled fieldsets — Period and Overlays controls grouped into
<fieldset class="ctrl-group">elements side-by-side in.controls-row. - W-0082: Accessibility — skip link,
aria-label="Site"on nav,id="main-content"on all<main>,aria-labelon all sections. Skip-link CSS added to theme.css. - W-0083: Consistent nav —
<a href="index.html">Dashboard</a>added as first nav-links item on all inner pages.aria-current="page"on active links. - W-0084: Disclosures page reorder — Coverage summary now comes before metric charts (new order: coverage → charts → statements).
- W-0085: Subtitles and captions —
class="page-subtitle"on all pages;<caption>on snapshot and disclosures tables;scope="col"/"row"on headers and bank cells. - W-0086: Mobile table scroll —
table-scroll-wrapclass withoverflow-x: autoandmin-width: 600px; sticky first column for snapshot table. - W-0087: Standardised footer — consistent
<nav aria-label="Footer">+ attribution on all four pages.footer navstyles added to theme.css. - W-0088: Persist bank selection —
localStorage["selectedBanks"]read on init, saved on every change. Period range was already persisted.
Mini-retro:
- Did the process work? Yes — all 10 items implemented in one pass; 192 Python tests still pass.
- What slowed things down? Nothing material; all changes were pure HTML/CSS/JS.
- Is this a pattern? Yes — Phase 19 items were individually small; future UI-only phases should be committed as a single batch rather than tracked as 10 separate items.
2026-04-28 — Frontend enhancements: default metrics, top 6 banks, historical charts (W-0017, W-0018)
Completed:
- W-0017: Default KPI metrics updated to Cost to Income Ratio, Return on Equity, Return on Assets, NIM. Top 6 standalone banks (ANZ, ASB, BNZ, Westpac, Kiwibank, Rabobank) set as default view. Cost to Income computed client-side from stored income components.
- W-0018: Chart.js 4.x historical time-series charts added (2×2 grid, one per key metric). All 32 quarters (2018-Q1 → 2025-Q4) rendered as line charts per bank with hover tooltips.
- Added three new RBNZ income series to
config/metrics.yaml: Trading and Hedging Gains (DBB.QIE50), Fees and Commission Income (DBB.QIE55), Other Income (DBB.QIE57). - Processed data regenerated: 13,900 rows (was 11,815; +2,085 from 3 new series).
- Glossary updated: Trading and Hedging Gains, Fees and Commission Income, Other Income, Cost to Income Ratio (with ADR-0001 derivation note).
- Bank selector with brand colours, checkbox pills, and quick-select buttons (Top 6 / All / Standalone only).
- Latest Quarter Snapshot comparison table (banks as rows, key metrics as columns, ▲/▼ trend vs previous quarter).
Completed:
- S-0001: RBNZ XLSX structure fully investigated. Findings in
learnings.md. 112 metric columns, 22 institutions, 2018-Q1 to 2025-Q4. LCR not available. Series IDs confirmed as stable mapping key. - S-0002: Additional bank disclosure sources (PDFs) assessed. Explicitly deferred — results in
learnings.md. - S-0003: Metric inconsistencies investigated — group vs. standalone entities, small-bank coverage gaps. No schema changes needed. Documented in ADR-0004 and
learnings.md. - W-0006:
config/metrics.yamlpopulated with 20 RBNZ series ID → canonical metric mappings. - W-0008: ADR-0004 written — formalises canonical schema, XLSX mapping strategy, period format, and missing metric documentation.
- W-0010: Full processing pipeline built:
src/processing/parse.py— XLSX parsing, normalisation (wide→long), duplicate detection, missing-value loggingscripts/process_data.py— entry-point script with fallback path resolution.github/workflows/process-data.yml— workflow triggered manually or after successful fetch- 34 new tests added (66 total, all passing)
data/processed/metrics.csvanddocs/data/processed/metrics.jsongenerated (11,815 rows)
- W-0014: Frontend updated with real data:
- Fixed
DATA_URLfor GitHub Pages compatibility (data/processed/metrics.jsonrelative todocs/) - KPI tiles with ▲/▼ trend indicators for CET1 Ratio, NIM, Core Funding Ratio (CFR), NPL Ratio
- Period and entity filter dropdowns
- Fixed
glossary.mdextended with 12 new RBNZ-specific metric definitionsdocs-adr/README.mdindex updated with ADR-0004
Remaining open:
- W-0013: Configure GitHub Pages in repo settings (one-time manual step)
- W-0016: Validate automated fetch workflow end-to-end against live RBNZ URL
Completed:
- Repository structure initialised following
davidamitchell/Researchconventions .github/skills/submodule added pointing todavidamitchell/Skills.github/copilot-instructions.mdcreated with language constraints and operational rulesBACKLOG.mdpopulated with all phases and work itemsPROGRESS.mdinitialised (this file)learnings.mdinitialised with structureglossary.mdcreated with all KPI categories and definitionsdocs-adr/README.mdcreated with index and template- ADR-0001: Data format decision (CSV for persistence)
- ADR-0002: Directory structure (aligned to Research repo, with documented deviations)
config/sources.yamlpopulated with RBNZ dashboard XLSX sourceconfig/metrics.yamlcreated as placeholderpyproject.toml,Makefile,.python-version,requirements.txtcreatedsrc/logger.py,src/config.py,src/ingestion/fetch.pycreatedtests/test_config.py,tests/test_ingestion.pycreated — all tests pass.github/workflows/ci.yml— lint and test on every push/PR.github/workflows/fetch-data.yml— manual trigger to fetch RBNZ XLSX.github/workflows/deploy-pages.yml— deploydocs/to GitHub Pagesdocs/index.html— static frontend that consumesdata/processed/metrics.json
What was done:
-
S-0004 spike: Authored
scripts/spike_bnz_pdf.pyto download and inspect the BNZ September 2024 full-year disclosure statement PDF usingpdfplumber. The script saves structured findings todata/raw/financial_disclosures/bnz/spike_s0004_bnz_findings.json. Spike must be run manually (network access required); findings template recorded indocs/spikes/s-0004-findings.md. -
URL research: All six bank disclosure statement URLs researched across 2018–2024 reporting periods. Total 82 report URLs catalogued:
- ANZ: 14 confirmed, 0 pending
- ASB: 13 confirmed, 0 pending
- BNZ: 11 confirmed, 3 pending (2023-03-31, 2021-03-31, 2018-09-30)
- Westpac: 13 confirmed, 1 pending (2024-09-30 — may not yet be published)
- Kiwibank: 4 confirmed, 9 pending (inconsistent filenames; all pending must be HTTP-validated)
- Rabobank: 12 confirmed, 2 pending (2024-12-31, 2024-06-30)
-
Config written:
config/sources.yamlupdated with three top-level sections:rbnz(unchanged),ocr(new — RBNZ B2 series for OCR/interest rate context), andfinancial_disclosures(new — 6 banks with full report lists). -
Validation script:
scripts/validate_disclosure_urls.pywritten. Performs HTTP HEAD requests against all disclosure URLs and updatesstatusfields in YAML. Writes per-URL results todata/raw/financial_disclosures/url_validation.json. -
Tests:
tests/test_validate_disclosure_urls.pyadded — 9 tests covering 200/404/ connection-error/timeout cases and JSON output structure. No real network calls. -
Dependencies:
requests>=2.32.0andpdfplumber>=0.11.0added topyproject.tomlandrequirements.txt.
What was found / decided:
- Three distinct BNZ URL patterns exist across years (2023+, 2021–2022, 2018–2020);
documented in
config/sources.yamlinline comments. - Kiwibank uses a media subdomain with inconsistent naming — no clean template pattern. All inferred URLs must be HTTP-validated before ingestion.
pdfplumberis sufficient for the spike (no LLM dependency decision yet). ADR will only be written after the spike is run and machine-readability is assessed.
Blockers / open questions:
- Spike script requires live internet access and a valid BNZ PDF URL — must be run
manually.
docs/spikes/s-0004-findings.mdis a template pending those results. - 16 pending URLs (BNZ ×3, Westpac ×1, Kiwibank ×9 + recount, Rabobank ×2) must be
validated by running
scripts/validate_disclosure_urls.py. - ADR-0005 (extraction approach) deferred until spike findings are reviewed.
Mini-retro:
- Did the process work? Yes — URL research, config authoring, and tooling all completed in one session with no backtracking.
- What slowed things down? Kiwibank filenames are non-deterministic (random suffixes in older files, capitalisation inconsistencies). Required manual pattern inference.
- Single change to prevent that next time? Add a URL validation step (now done via
scripts/validate_disclosure_urls.py) as a required gate before any URL entersconfig/sources.yamlasconfirmed. - Is this a pattern requiring a root-cause fix? Yes — the root cause is that banks do not follow consistent URL conventions. The validation script is the structural fix; add it to the fetch workflow as a pre-check step (backlog item).
Items completed:
src/processing/extract_disclosures.py: text extraction from bank disclosure PDFs usingpdfplumber. Extracts 10 quantitative metrics (Net Interest Income, Total Operating Income, Operating Expenses, Profit After Tax, Total Assets, Net Loans and Advances, Deposits, Equity, CET1 Ratio, Total Capital Ratio). Normalises: brackets = negative, comma thousands, NZD thousands scale (÷1000). Outputs canonical rowsentity | metric | value | period | source. Only processes PDFs with a.meta.jsonsidecar.scripts/process_disclosures.py: writesdata/processed/disclosures.csvanddocs/data/processed/disclosures.json.tests/test_extract_disclosures.py: 34 tests covering happy path, bracketed negatives, NZD thousands normalisation, missing sidecar, empty extraction, source field, period format.
src/processing/parse_ocr.py: reads RBNZ B2 XLSX, detects OCR column (case-insensitive), converts monthly → quarterly (last value per quarter), outputs canonical rowsentity=RBNZ | metric=OCR | source=rbnz-ocr. Falls back to anyrbnz-ocr*.xlsxindata/raw/if primary path absent.scripts/process_ocr.py: writesdata/processed/ocr.csvanddocs/data/processed/ocr.json.docs/index.html: loadsocr.jsonalongsidemetrics.json. NIM chart gains a teal dashed secondary y-axis line (OCR %) on the right side. Graceful degradation: NIM chart renders normally ifocr.jsonfetch fails.tests/test_parse_ocr.py: 20 tests covering happy path, monthly-to-quarterly conversion, missing file, no OCR column, fallback glob, canonical schema.
docs/index.html: date-range filter bar above charts with four presets (Last 4Q,Last 8Q,Last 16Q,All). Active button styled teal border (#00C3A5). Clicking a preset filters ALL charts and the snapshot table client-side. Preset persists inlocalStorage["rangePreset"]and is restored on page load. Entity filter operates independently.
W-0016 — marked wont-do: Kiwibank and Westpac CDNs block pipeline downloads (WAF / timeout). Not fixable without manual intervention.
Mini-retro:
- Did the process work? Yes — spike S-0004 findings were precise enough to design the extraction regex patterns without reading any additional PDF pages.
- What slowed things down? Capital ratio extraction needed a "second percentage" strategy distinct from the "first value" strategy used for income/balance sheet metrics. This required a separate extraction path.
- Single change to prevent that next time? Document the two extraction strategies (first_value vs second_pct) in the spike output before implementation begins, so the distinction is explicit.
- Is this a pattern? Yes — financial disclosure PDFs consistently have a minimum-requirement column before the actual bank ratio column. Document this in
learnings.md.
2026-05-01 — W-0049, W-0033, W-0030, W-0024: ADR-0005, entity_type, new metrics series, disclosure pipeline run
Items completed:
docs-adr/0005-pdf-extraction-approach.md: Documents the extraction architecture (pdfplumberextract_text()+ two-strategy regex approach). Captures thefirst_valueandsecond_pctstrategies, unit detection logic, known gaps (Rabobank image-based, Westpac WAF), four rejected alternatives (extract_tables, LLM, OCR, manual entry), and implementation notes including the TDD workflow for adding new metrics.docs-adr/README.md: ADR-0005 entry added to index.
src/processing/parse.py: Added_GROUP_ENTITIESfrozenset classifying 7 group entities (ANZ Group, BOC Group, CBA Group, CCB Group, ICBC Group, Rabo Group, WBC Group). Every row now includesentity_type: "standalone" | "group". CSV writer updated to includeentity_typein field list.docs/data/processed/metrics.json+data/processed/metrics.csv: Regenerated withentity_typefield.docs/index.html: "Standalone only" button now derives group entities fromentity_typefield in data rather than hardcoded name list.docs-adr/0004-rbnz-data-contract.md: Schema table and Decision section updated to document the new field.tests/test_processing.py: 4 new tests (TDD Red→Green): standalone entity type, group entity type, unknown defaults to standalone, all 7 known group entities classified correctly.
config/metrics.yaml: Added 5 confirmed RBNZ series:DBB.QIH10(1-Month Mismatch Ratio),DBB.QIH20(1-Week Mismatch Ratio),DBB.QIJ10(Top 5 Non-Bank Credit Exposures),DBB.QIJ30(Top 5 Bank Credit Exposures),DBB.QIJ40(Bank Exposures ≥10% of CET1).docs/data/processed/metrics.json+data/processed/metrics.csv: Regenerated — 13900 → 17375 rows (+3475 new rows for 5 series).glossary.md: 5 new metric definitions added with RBNZ series IDs and units.
scripts/process_disclosures.py: Fixed output path fromdisclosures.json(the existing PDF index file) todisclosure_metrics.json(distinct metrics file). Added_log_extraction_summary()to log extracted/null counts per bank per period.- Script run against full corpus (ANZ ×3, ASB ×16, BNZ ×6, Kiwibank ×13, Rabobank ×3 = 41 PDFs).
- Outputs:
data/processed/disclosures.csvanddocs/data/processed/disclosure_metrics.json.
Mini-retro:
- Did the process work? Yes — TDD cycle caught the CSV header regression immediately (existing test for field list needed updating for the new
entity_typefield). Fixed in the same cycle before the commit. - What slowed things down? The process_disclosures.py output path bug (writing to
disclosures.jsonwhich overwrites the PDF index) was found only when reading the W-0024 backlog item carefully. The naming should have been caught when W-0015 was initially implemented. - Single change to prevent this next time? When a script produces a new output file, check whether the path conflicts with existing files in the same directory before choosing the name. Add this as a code-review checklist item.
- Is this a pattern? Yes — naming ambiguity between index files and metrics files. Document in copilot-instructions.md: processed output files should use descriptive names (
_metrics,_index) to avoid collision.
Items completed:
Root cause of ASB "Net Loans and Advances" gap: ASB uses "Advances to customers" as the balance sheet label, not "Loans and advances". TDD cycle:
- Red: 2 tests using
_BALANCE_METRICSproduction patterns fail (result=None on ASB label line) - Green:
r"advances to customers\b"added to_BALANCE_METRICSpatterns for "Net Loans and Advances" - Full suite: 152 tests pass, 0 regressions
- ASB extraction: 125 → 140 rows; total disclosure rows: 344 → 359
config/metrics.yaml:DBB.QIB90(Total Risk-Weighted Assets) addedglossary.md: Total Risk-Weighted Assets, RORWA, Risk Density definitions addeddocs/index.html: RORWA (PAT/RWA×100) and Risk Density (RWA/Loans×100) computed inbuildLookup(); added toKEY_METRICSandMETRIC_LABELSmetrics.json/metrics.csv: 17375 → 19460 rows
config/metrics.yaml:DBB.QIC60(Individual Provisions),DBB.QIC70(Collective Provisions) addedglossary.md: Individual Provisions, Collective Provisions, Provisioning Coverage, Provision Charge definitions addeddocs/index.html: Provisioning Coverage ((IndProv+CollProv)/NPL×100) computed client-side and added to chart grid
Mini-retro:
- Did the process work? Yes — the ASB regex fix followed the TDD cycle correctly; the second Red test (using
_BALANCE_METRICSdirectly from production code) was a genuine Red before the fix. - What slowed things down? First TDD attempt for W-0074 used the fixed patterns in the test parameters, making the test pass immediately (not a true Red). Caught and corrected before proceeding.
- Single change to prevent this next time? When testing extraction patterns, import
_BALANCE_METRICS(or the equivalent production constant) in the test rather than hardcoding patterns in the test parameter. This ensures the test exercises the production code paths and will be Red when the production pattern is wrong. - Is this a pattern? Yes — when unit-testing configurable behaviour (like regex pattern lists), always import the production config into the test rather than duplicating it.
Items completed:
docs/index.html: LDR = (Net Loans / Deposits) × 100 computed inbuildLookup(). Added toKEY_METRICSandMETRIC_LABELS. Renders as a chart alongside existing metrics. No new data pipeline work — both series already inmetrics.json.glossary.md: Loan-to-Deposit Ratio definition added.
docs/index.html:verticalLinesPlugin(custom Chart.js plugin, registered globally) draws dashed vertical lines via canvasafterDraw. Red = OCR hike, teal = OCR cut. Threshold: |delta| ≥ 0.25 (25 bps).buildOcrEvents()computes quarterly OCR deltas from already-loadedocr.json. "OCR rate changes" checkbox toggle (off by default); preference persisted inlocalStorage.
config/events.yaml: 13 curated events (2019-Q4 through 2024-Q3) covering RBNZ capital reform, COVID, FLP, LVR cycles, OCR peaks, SVB/Credit Suisse, Kiwibank govt buyback. Category colour scheme: teal=monetary, amber=regulatory, grey=macro/market.docs/index.html:NZ_EVENTSandEVENT_COLORSinlined as JS constants for static-site delivery. "NZ events" checkbox toggle (off by default). Both OCR event and NZ event annotation layers merged and passed toverticalLinesPluginper chart render cycle.
docs/index.html: Freshness badge below the status line shows "RBNZ data: YYYY-QN · Disclosures: YYYY-QN". RBNZ latest derived fromallPeriods.last. Disclosure latest derived from loadeddisclosure_metrics.json. Badge is teal-coloured; failure to load disclosures is non-fatal.
Mini-retro:
- Did the process work? Yes — all four items were purely client-side and did not require pipeline changes. The vertical annotation plugin approach (custom Chart.js plugin using
afterDraw) is clean and avoids external dependencies. - What slowed things down? The NZ events toggle required merging two annotation layers (OCR events + NZ events) before passing to the plugin. Initial implementation passed them separately, requiring a merge step.
- Single change to prevent that next time? Design the annotation layer API to accept a unified array from the start rather than retrofitting it.
- Is this a pattern? Yes — when adding multiple overlays that share the same rendering path, define the unified data contract first.
2026-05-01 — W-0042, W-0020, W-0041, W-0037, W-0043, W-0025–W-0029, W-0054: chart tabs, UX improvements, disclosure charts
Items completed:
docs/index.html:TABSconst maps 4 tab categories to metric lists (profitability / capital / asset quality / liquidity).KEY_METRICSderived as the flat union. Tab bar (.tab-bar/.tab-btn) rendered above chart grid; active tab persists inlocalStorage["activeTab"].renderChartsfilters toTABS[activeTab]metrics only.METRIC_LABELSextended with 11 new entries covering all newly-visible series.
docs/index.html:⛶button per chart card opens#chart-modal-overlay(fixed, full-viewport).openFullscreen(idx)clones chart config viastructuredClone, re-attaches tooltip callback fromchartMeta, creates a freshChartinstance on#modal-canvas.closeFullscreen()destroys modal chart. Escape key and overlay background click both close.
docs/index.html:↓button per card (and in modal).downloadChart(idx)draws chart canvas onto an offscreen canvas with#0d0d0dfill, exports viacanvas.toDataURL('image/png')→ programmatic<a download>click. Slug derived from metric name.
docs/index.html: "Sector avg" checkbox added to filter bar.getStandaloneEntities()usesentity_type === 'group'detection with STANDALONE fallback. Sector avg dataset (dashed grey#888) added per chart when ≥2 standalone visible banks have data for a period. Nulls excluded from mean. Persists inlocalStorage["showSectorAvg"].
docs/index.html: (1) Click-to-sort column headers —sortStateupdated on click,▲/▼CSS suffix added via.sort-asc/.sort-descclasses; (2) Best/worst cell colouring —.cell-best(teal) and.cell-worst(red) applied per column; (3) Period selector dropdown above table —#snapshot-period-selpopulated fromallPeriods, persists inlocalStorage["snapshotPeriod"].
docs/disclosures.html: Chart.js CDN added. New#disc-charts-sectioninserted before existing content. Six charts rendered fromdisclosure_metrics.json:- W-0025: Line charts for Profit After Tax, Operating Expenses (abs), Total Assets, CET1 Ratio — one line per bank, all periods
- W-0026: Grouped bar chart (income statement) for latest full-period per bank: NII, Non-Interest Income, OpEx abs, PAT
- W-0027: Operating Expenses over time (line chart, abs values)
- W-0028: Capital ratios (CET1 + Total Capital Ratio as solid/dashed lines per bank)
- W-0029: Loans & Deposits (Net Loans + Deposits as solid/dashed lines per bank; Rabobank null noted)
- Already in
config/metrics.yaml(added by W-0030). Now visible as the bottom three series in the Liquidity tab (W-0042).METRIC_LABELSextended for all three series. Glossary already had no entries for these three — added via the W-0030 progress note.
Mini-retro:
- Did the process work? Yes — all items were client-side only; no Python changes needed.
- What slowed things down? The fullscreen modal required
structuredCloneof the Chart.js config to avoid shared state; tooltip callbacks (functions) are not clonable and needed to be stored separately onchartMetaand reattached. - Single change to prevent next time? When designing chart config storage, separate serialisable config from non-serialisable callbacks from the start.
- Is this a pattern? Yes — any time chart configs need to be duplicated (e.g. for export, fullscreen, or print), treat the callback functions as a separate layer attached after cloning.
2026-05-02 — File naming alignment, CHANGELOG, and copilot instructions improvements (W-0068, W-0089, W-0090)
Completed:
- W-0089:
backlog.md→BACKLOG.md,progress.md→PROGRESS.md— aligns withdavidamitchell/Researchconvention already specified in ADR-0002. All references updated (README.md, learnings.md, copilot-instructions.md, BACKLOG.md itself). - W-0068:
CHANGELOG.mdcreated following Keep a Changelog 1.0.0. Historical entries backfilled for the initial scaffold, first processed data, disclosure pipeline, OCR overlay, and Phase 19 UI. Future schema changes must include a CHANGELOG entry as a hard gate. - W-0090:
.github/copilot-instructions.mdcomprehensively updated following a review ofdavidamitchell/Latest-developments-/.github/copilot-instructions.md:- Skills section moved to the top of the file
- Explicit Skill Chains table added
BACKLOG.md,PROGRESS.md,learnings.md,CHANGELOG.md, and ADR mandates added as standalone sections- Slice Completion Checklist added (hard gates: make check, make test, PROGRESS.md, CHANGELOG.md if schema change, ADR if warranted, glossary if new metric)
decisionsskill added to the skills table (maps toadr)- "Root Cause Before Action" section added
- Previous "Continuous Improvement" and "Systemic Improvement Principles" sections consolidated
- All
backlog.md/progress.mdlowercase references updated to uppercase
Mini-retro:
- Root cause of the file naming non-conformance: ADR-0002 specified the standard but the initial scaffold implementation was never checked against it. Fix: Slice Completion Checklist now includes an ADR compliance gate, and the file naming standard is explicitly enforced by the instructions.
- Root cause of the missing CHANGELOG: W-0068 was in the backlog but not yet executed. The new copilot instructions make CHANGELOG maintenance a hard gate for every schema-changing item.
- Root cause of the instructions gap: the reference repo (
Latest-developments-) had evolved since this repo was scaffolded and the divergence was not tracked. Fix: ADR mandate now includes a rule to review the reference repo when making instruction changes.
What changed:
- W-0061: Added monthly schedule trigger (
0 6 1 * *) tofetch-data.ymlalongsideworkflow_dispatch. Updated comment to reflect both triggers. - W-0070: Added YAML front matter (
title,status,date,authors,tags,supersedes,superseded_by) to ADR-0001 through ADR-0004. Added coded consequence bullet IDs (POS-001, NEG-001, NEU-001) to all four ADRs. - W-0071: Added
## Code Review Standardsection to.github/copilot-instructions.mdwith six-point checklist: correctness, data accuracy, test coverage, security, idempotency, ADR compliance. - W-0032: Created
config/capital_requirements.yamlwith the RBNZ 2019 capital reform phase-in schedule (2023–2028, D-SIB and non-D-SIB). Added Capital Headroom derived metric tobuildLookup()indocs/index.html, using the phase-in schedule and D-SIB surcharges (ANZ/Westpac +1.5%, ASB/BNZ +1.0%). Added to capital tab, METRIC_LABELS, and METRIC_INFO. - W-0055: Added Pre-Provision Profit (PPP = NII + Trading + Fees + Other − Opex) to
buildLookup(). Added to profitability tab, METRIC_LABELS, METRIC_INFO. - W-0056: Added Non-Interest Income Share (%) to
buildLookup(). Added to profitability tab, METRIC_LABELS, METRIC_INFO. - W-0057: Added
buildProvisioningMetrics()function for Credit Impairment Rate (ΔNPL ÷ Net Loans × 100). Called fromrender(). Added to asset quality tab, METRIC_LABELS, METRIC_INFO. - W-0058: Added Opex Intensity (bps) to
buildLookup()(quarterly opex × 4 ÷ total assets × 10000). Added to profitability tab, METRIC_LABELS, METRIC_INFO. - W-0062: Added
syncUrlHash()andloadFromHash()for hash-based URL state encoding (banks, range, tab, mode). Added "📋 Copy link" button with Copied! feedback. - W-0063: Added
downloadCsv()function and "↓ CSV" button. Exports filtered data in canonical schema format. - W-0040: Added Indexed (base=100) chart mode toggle. Rebases all non-OCR datasets to 100 at first non-null value. Stored in
localStorage["chartMode"]. Shows informational note when active. - W-0047: Added auto-narrative panel (
#narrative-section) between KPI row and charts. Generates 4–5 plain-English bullets: NIM trend, ROE leader, CET1 direction, C/I trend, NPL trend. Collapsible with localStorage state. - W-0021: Created
docs/methodology.html(KPI formulas, RBNZ series table, derived metrics, capital reform schedule, data currency) anddocs/lineage.html(data model, pipeline stages, source files, disclosure pipeline). Added Methodology and Lineage nav/footer links to all existing pages.
Tests: 192 passed (no regressions).
Mini-retro:
- Batch execution of 13 items in one session was feasible because all items were well-specified with concrete formulas and clear scope. The main risk was JS syntax consistency in the large index.html file — mitigated by using a sub-agent with full context.
- Root cause of complexity: index.html is a monolithic file. Future work should assess whether splitting into modules would reduce the cognitive load per change.
Five items implemented in a single session.
- W-0045: Added "Band (min/max)" checkbox to Overlays fieldset. When enabled with ≥2 standalone banks visible, renders a semi-transparent teal shaded band (
#00C3A508fill) between the min and max values across standalone banks for each period. Uses two hidden_band_min/_band_maxdatasets with afill: '-1'Chart.js band fill. Legend entries starting with_band_are filtered from display. - W-0046: Added
#ranking-sectionbelow the snapshot table.renderRankingTable()shows rows = banks, columns = KEY_METRICS with data in the selected period. Rank view colours top quartile teal (.rank-best) and bottom quartile red (.rank-worst). A "Value view" / "Rank view" toggle button persists state inlocalStorage["rankingViewMode"].HIGHER_IS_BETTERset drives ranking direction for 12 metrics; lower-is-better for the remaining 11. - W-0048: Added "Trend flags" checkbox to Overlays fieldset. When enabled, appends
▲/▼/→to each bank's chart legend label based on the trailing slope of the last 4 data points. Stable threshold:|slope| < 0.05 × stdDev(or 0.1 if stdDev = 0). Direction is interpreted viaHIGHER_IS_BETTER.trailingSlope()helper added. - W-0051: Replaced the terse
#capital-reformsection indocs/methodology.htmlwith a richer version: narrative introduction, restructured CET1 requirements table (Period / Non-D-SIB / D-SIB base / surcharges), conservation buffer explanation, and Capital Headroom linkage. - W-0059: Added
--batchflag toscripts/download_disclosures.py(existing batch behaviour is now explicitly invoked via this flag). Created.github/workflows/download-disclosures.ymlwithworkflow_dispatchinputs forbankandforce. Added 6 tests for the--batchflag covering: acceptance, all-banks processing, bank filter, force pass-through, partial failure exit code, and non-abort behaviour on single failure.
Tests: 198 passed (no regressions). Lint clean.
Added quarterOffset(), linearRegression(), and renderNimPassThrough() to docs/index.html. The scatter chart (OCR change vs NIM change 2Q later, per bank) appends to the profitability tab chart grid when OCR data is loaded. Shows per-bank trend lines via linear regression. Falls back to a placeholder card when OCR data is absent.
Created docs/coverage.html with two sections: (1) RBNZ quarterly coverage table — rows = RBNZ-stored KEY_METRICS, columns = Top 6 banks, cells = present/total periods colour-coded teal/amber/red; (2) Disclosure coverage table — rows = banks, columns = key disclosure metrics, shows ✓/— with WAF and image-based notes. Coverage nav link added to all existing pages (index, glossary, disclosures, methodology, lineage, data-sources).
Created docs/snapshot.html — 3-column bar-chart grid (one chart per KEY_METRIC, all standalone banks side-by-side) with a quarter selector dropdown. Duplicates the derived-metrics logic from index.html for client-side computation. "Full snapshot →" link added to snapshot section title in docs/index.html.
Added computeAnomalies() which computes trailing 8Q mean and stddev per bank/metric and flags values > 2σ. Anomaly ⚠ icons (amber, with tooltip showing σ distance and trailing average) applied to snapshot table and ranking table value view. CSS .anomaly-flag added.
Implemented the full Phase 18 Labour and Customer Productivity Metrics chain.
Reference data (S-0012/S-0013/W-0075): Created data/reference/employees.csv and data/reference/customers.csv with annual FTE and active customer estimates for ANZ, ASB, BNZ, Westpac, Kiwibank, and Rabobank from 2018 to 2024. FTE data sourced from KPMG FIPS (exact for 2022/2023) and triangulated from bank annual reports for earlier years. Customer data estimated from market share proxies and NZBA Retail Banking Insights. Confidence field (exact / triangulated / estimated) propagated through to outputs. Schema documented in config/reference.yaml.
Processing module (W-0076): src/processing/compute_productivity.py computes six metrics:
- Profit per Employee (NZD/FTE, annualised)
- Gross Income per Employee (NZD/FTE, annualised)
- Expenses per Employee (NZD/FTE, annualised)
- Profit per Customer (NZD, annualised)
- Gross Income per Customer (NZD, annualised)
- Expenses per Customer (NZD, annualised)
Quarterly NZDm P&L values are annualised ×4 before dividing by the reference denominator. Reference lookup uses the most recent annual data point at or before the quarter end date — this correctly handles mid-year quarters for Sep-year-end banks (e.g. 2023-Q1 uses the 2022-09-30 FTE figure). Group entities are excluded. Missing denominators produce WARNING logs and no output row. scripts/compute_productivity.py entry point writes data/processed/productivity.csv and docs/data/processed/productivity.json. 20 tests in tests/test_compute_productivity.py.
Workflow (W-0077): .github/workflows/compute-productivity.yml runs after Process Data and commits output files idempotently using git diff --cached --quiet guard.
Frontend (W-0078): Productivity tab added to docs/index.html. Six line charts rendered from productivity.json via renderProductivityCharts(). NZD values formatted with Intl.NumberFormat. Confidence badges (🔵 🟡 🔴) appear in tooltips. If productivity.json is absent, the tab shows a plain-language message directing the user to run the workflow. Graceful degradation: banks without reference data are silently omitted from that metric's chart.
User requested: "Keep going — I want those efficiency metrics."
data/reference/employees.csv(new)data/reference/customers.csv(new)config/reference.yaml(new)src/processing/compute_productivity.py(new)scripts/compute_productivity.py(new)tests/test_compute_productivity.py(new, 20 tests).github/workflows/compute-productivity.yml(new)docs/index.html(Productivity tab + PRODUCTIVITY_URL + metric labels + state var)CHANGELOG.md,BACKLOG.md,PROGRESS.md,learnings.md
BACKLOG.md extended with a new Phase 20: Advanced Analytics and Strategy Alignment (Additive) section containing ten new backlog items (W-0091 through W-0100). The Priority Stack was also updated to list Phase 20 items and their recommended sequencing.
All ten items are strictly additive — no existing tabs, charts, filters, or exports are modified. All new features default to off or hidden on first load.
| Item | Title | Status |
|---|---|---|
| W-0091 | Controllability metadata in config and glossary | ready |
| W-0092 | Metric Grouping filter (additive dropdown) | ready |
| W-0093 | Golden Metrics panel (collapsible) | ready |
| W-0094 | Correlations tab with interactive heatmap | ready |
| W-0095 | Market-Adjusted Comparison view | ready |
| W-0096 | Strategy Scorecard tab (purpose pillars) | ready |
| W-0097 | Vision Alignment Report (exportable CSV) | ready |
| W-0098 | Dimensionality Reduction tab (PCA, lazy loaded) | ready |
| W-0099 | Enhanced tooltips (controllability + strategy notes) | ready |
| W-0100 | Analysis Tools navigation entry | ready |
Issue requested: decompose and refine the Phase 20 feature set into clean, coherent, additive backlog items. Items are sequenced by dependency (W-0091 config unblocks W-0092, W-0096, W-0099; W-0096 unblocks W-0097; W-0094/W-0095/W-0096/W-0098 unblock W-0100).
- Added
scorecardtab todocs/index.htmlTABS constant (special tab withnullmetric list, dispatched fromrenderCharts()). renderScorecard()function renders five pillar cards (Customer, Efficiency, Resilience, Innovation, Returns) from theSTRATEGY_PILLARSconstant.- Multi-bank mode (2+ banks selected): each metric becomes a section header with one row per selected bank showing coloured swatch, bank name, individual value, QoQ trend arrow, and a mini proportional bar. No averaging.
- Single-bank mode: each metric shows value, QoQ trend, peer-percentile bar relative to all banks in the dataset, and lead/lag badge.
- Export CSV button: per-bank rows with
bank,pillar,metric,latest_value,prior_value,trend,leading_or_lagging. - Print / Save as PDF button via
window.print()with print stylesheet. - Created
config/strategy_pillars.yamlas authoritative YAML source (inlined to JS for static-site delivery). - ADR-0007 documents the per-bank vs sector-average design decision and the choice of table rows over grouped bar charts.
PR #35 revealed that the scorecard averaged data across selected organisations, destroying the comparative signal that the rest of the dashboard is built around. The fix is architectural: the scorecard must show individual bank values in multi-bank mode, consistent with every other tab.