## Demo
Proof that the AERIE-439 surfaces work: the vendor-breakdown reducer (spec 24) was run directly against fixtures and its real output is embedded below; the Opus 4.8 commentary action and the live Redshift actions (specs 24/25) are not invoked in CI (mutation-free rule — Anthropic + Redshift calls), so their reproduction commands are embedded instead; the drill-down panel + commentary UI (spec 26) get reviewer click-through steps with a screenshot placeholder.
Backend — vendor-breakdown reducer (spec 24, reduceVendorBreakdownForCell)
Ran a throwaway vitest spec that imports reduceVendorBreakdownForCell from chat/convex/finance/dashboards/perSchoolReducers.ts and calls it directly with fixtures (no DB, no network, no mutation):
=== HAPPY PATH (1 cell, 4 txns → 3 vendor groups + residual) ==={
"lineTotal": 1000,
"sumItemized": 950,
"unitemizedAmount": 50,
"prefixWarning": false,
"vendors": [
{ "vendorKey": "acme co", "vendorName": "Acme Co", "total": 500, "txnCount": 2 },
{ "vendorKey": "beta llc", "vendorName": "Beta LLC", "total": 400, "txnCount": 1 },
{ "vendorKey": "unknown vendor", "vendorName": "Unknown vendor", "total": 50, "txnCount": 1 }
]
}
--- INVARIANTS ---
Σ vendors[].total + unitemizedAmount = 1000 (== lineTotal 1000)
vendors sorted by total desc: Acme Co $500 (2 txns); Beta LLC $400 (1 txns); Unknown vendor $50 (1 txns)
casing/whitespace collapse → Acme Co txnCount = 2
vendorless → "Unknown vendor" present: true
This shows the core contract: matched txns grouped by normalized vendor ("Acme Co" + "ACME CO" collapse into one group, txnCount 2), a vendorless txn lands in "Unknown vendor", rows sorted by total desc, and the tie-out invariant Σ vendors + unitemizedAmount === lineTotal holds (a $50 residual folds into unitemizedAmount).
Most at risk from this change — the edge branches the diff disturbs (prefix-warning, empty-account throw, no-matched-txns), driven directly and held:
=== BLAST RADIUS 1 — no numeric prefix in accountName ===prefixWarning: true | vendors: []
=== BLAST RADIUS 2 — empty accountNames throws ===
thrown message: "accountNames must be non-empty"
=== BLAST RADIUS 3 — no matched txns ===
vendors: [] | unitemizedAmount: 750 (== lineTotal 750)
Regression net — the committed test suites for the touched backend modules pass:
✓ convex/finance/dashboards/perSchoolReducers.test.ts (22 tests)✓ convex/finance/dashboards/modelCoverageCommentaryShared.test.ts (15 tests)
Test Files 2 passed (2) · Tests 37 passed (37)
Backend — live Redshift + Opus actions (specs 24/25) — NOT executed in CI
These hit Redshift and the Anthropic API and so are not run here (read-only/non-mutating rule + no prod target). Reproduction (local dev shell, with .env + ANTHROPIC_API_KEY set):
# Vendor breakdown for a real Programs cell (spec 24 live action):npx convex run finance/dashboards/financialLive:getVendorBreakdownForCellLive \
'{"school":"Alpha Miami","period":"2026-Q1","accountNames":["70100 Curriculum"]}'
# AI commentary digest → Opus 4.8 (spec 25 action; force-bypass cache):
npx convex run finance/dashboards/financialLive:generateModelCoverageCommentary \
'{"period":"2026-Q1","force":true}'
Expected: the vendor action returns the same shape proved above over live Redshift rows (auth via requireSchoolPlAccess first); the commentary action returns { source: "claude", bullets: [...5–6...] } — or { source: "fallback", bullets: [<one bullet>] } when ANTHROPIC_API_KEY is absent (never throws).
UI — drill-down panel + AI commentary (spec 26)
1. Open Dashboards › Financials › Schools – Actual vs Model, select "All Schools" (consolidated view).
2. At the top, the Model coverage & variance band now shows an AI commentary block (Sparkles header) with a source badge — Opus 4.8, cached, or unavailable — and a flat 5–6 bullet executive summary (a 5-line skeleton appears while it generates).
3. In the "Model coverage by school" table, hover a row — it now shows a pointer cursor + chevron; click it.
4. A drill-down side panel slides in on the right (in-flow, not over the header), listing that school's biggest annualized cost line items across Headcount / Programs / Facilities, each with a magnitude bar and a "Top" badge on the top 3.
5. Expand a Programs/Facilities line → it shows aggregated vendor rows (name · $ · txn count, "Unknown vendor" handled). Expand a Headcount (Contracted-Labor) line → it shows aggregated contractor rows with "Unattributed" pinned last. The leaf is terminal (no per-transaction drill).
6. Press Escape or the X to close the panel; press Refresh — the P&L, the coverage band, and the commentary all re-run.
> _Screenshot: consolidated Actual vs Model page showing the AI commentary block in the band + the drill-down side panel expanded to vendor/contractor rows — _<!-- paste screenshot here -->
<img width="2624" height="1636" alt="image" src="https://github.com/user-attachments/assets/4bad9bee-56d1-425f-ac83-dc3c1e63ceb1" />
<img width="2624" height="1636" alt="image" src="https://github.com/user-attachments/assets/9ef32097-72b1-4cf0-b4d9-f1b1ac8882bb" />
<img width="2624" height="1636" alt="image" src="https://github.com/user-attachments/assets/56e66b85-433c-4689-a2ab-d22dc2a52481" />
---
## AERIE-439 — Consolidated Financials: Model-coverage drill-down panel + AI commentary
Two additions to the consolidated "Actual vs Model (Schools)" page (schools-avm, isConsolidated branch), both follow-ons to [AERIE-437](https://linear.app/builder-team/issue/AERIE-437) (model coverage & variance band):
1. Line-item drill-down side panel — clicking a row in the "Model coverage by school" table opens an in-flow side panel listing that school's biggest annualized cost line items across the three modeled sections (Headcount, Programs, Facilities/Support), annualized ×12/monthsLoaded, sorted descending, with a magnitude bar and a "Top" badge on the top three. Each line item expands to an aggregated terminal leaf (no per-transaction drill): vendors for Programs/Facilities, contractors (via the xoContractorIdentity alias dictionary) for Headcount/Contracted-Labor accounts.
2. AI commentary — an Opus 4.8 executive summary (flat 5–6 bullets, ~2-minute read) rendered inside the variance band, auto-generated on load and cached per period (24h TTL + variance-fingerprint invalidation). The digest feeds the model the same aggregated rollups (top vendors / top contractors for the single biggest line item), not raw transactions; falls back to a single explanatory bullet when ANTHROPIC_API_KEY is absent.
Linear: [AERIE-439](https://linear.app/builder-team/issue/AERIE-439) — Consolidated Financials: Model-coverage drill-down panel + AI commentary (vendor / contractor aggregated)
### Status — implemented + tested (ready for review)
All three specs are implemented, tested, and self-reviewed. The feature doc changelog and each spec's metadata Status are marked Completed.
### Specs (all Completed)
- 24 · vendor-breakdown-reducer-and-action _(backend, no deps)_
New pure, runtime-free reduceVendorBreakdownForCell in perSchoolReducers.ts — sibling of reduceContractorBreakdownForCell, minus the xoContractorIdentity identity dim. Same period + account-number-prefix matching and lineTotal math; groups matched transactions by normalized vendor (name = (vendorName ?? "Unknown").replace(/\s+/g, " ").trim(), key = lowercase; null/empty → "Unknown vendor"), returns { lineTotal, sumItemized, unitemizedAmount, prefixWarning, vendors: [{ vendorKey, vendorName, total, txnCount }] } (vendors sorted by total desc; unitemizedAmount tolerance-suppressed within UNITEMIZED_TOLERANCE_USD). New "use node" live action getVendorBreakdownForCellLive({ school, period, accountNames }) — twin of getContractorBreakdownForCellLive minus the identity read: requireSchoolPlAccess (auth before any Redshift connection) → parseDrilldownPeriod → fetchPlMonthlyRowsForSchool + fetchPlTransactionsForSchool → reducer. Compile-time AssignableTo parity guard added in financials-view.tsx. No new Redshift table/SQL.
- 25 · ai-commentary _(backend, no deps)_
New runtime-free modelCoverageCommentaryShared.ts (digest type carrying topContributors?: { name, amount, txnCount }[] + a vendors-vs-contractors flag; buildModelCoverageCommentaryPrompt; parseCommentaryBullets; fingerprintCommentary; COMMENTARY_MODEL = "claude-opus-4-8") + modelCoverageCommentaryStore.ts (internalQuery/internalMutation cache pair, 24h TTL, delete-then-insert upsert) + the generateModelCoverageCommentary Opus 4.8 action. The digest feed is switched to aggregated rollups: for the single biggest line item it loads top contractors (Contracted-Labor account) else top vendors; missing ANTHROPIC_API_KEY (or any Opus/parse failure) degrades to a single "fallback" bullet — never throws. New modelCoverageCommentary schema table { period, bullets[], source, fingerprint, generatedAt } indexed by_period; founding-family policy text fed in qualitatively via an internal query; npx convex codegen run (generate-only — no schema push).
- 26 · drilldown-panel-and-commentary-ui _(frontend, depends on 24 + 25)_
New model-coverage-line-item-panel.tsx (in-flow side panel matching the MappingsSidePanel / UnitemizedBreakdownSidePanel pattern — desktop flex child w-2/5, mobile MobileOverlayPanel gated by useMobileUI, wrapped in AnimatePresence, Escape + X close; per-account HC→contractor vs else→vendor terminal leaf with per-group loading/error/empty states) + new presentational model-coverage-commentary.tsx (Sparkles header + source badge: Opus 4.8 / cached / unavailable; 5-line skeleton; error/empty fallbacks; bullet list). model-coverage-by-school-table.tsx gains an optional onSelectSchool (clickable rows + chevron when present); model-coverage-variance-band.tsx gains an optional commentary? slot; financials-view.tsx holds drilldownSchool state, conditionally mounts the panel under AnimatePresence, fires the commentary action via useLiveSection, folds the commentary refetch into the consolidated Refresh, and adds parity guards. New frontend-safe isContractedLaborAccount in @bran/contracts/headcount-account-roles (regex /^\d+\s+Contracted Labor\b/) so the panel does not import the backend isHeadcountAccount.
### Implementation summary
- Backend (specs 24 + 25): one new pure reducer + one new live action (vendor breakdown), one new Opus 4.8 commentary action, two new runtime-free/shared modules (modelCoverageCommentaryShared.ts, modelCoverageCommentaryStore.ts), one new schema table (modelCoverageCommentary) + codegen, one new founding-family internal query. Both new live actions authorize via requireSchoolPlAccess before any Redshift connection; all "use node" DB I/O routes through internal query/mutation (an action has no ctx.db); reducers and shared helpers are runtime-free.
- Frontend (spec 26): two new components, three modified (model-coverage-by-school-table.tsx, model-coverage-variance-band.tsx, financials-view.tsx), one new frontend-safe predicate in @bran/contracts. All additions are backward-compatible (the band/table keep their original layout when the new optional props are omitted).
### Test coverage — 47 new tests, all green
- Spec 24: +9 in perSchoolReducers.test.ts (22 total) — grouping + sum + txnCount, vendor normalization (casing/whitespace collapse, "Unknown vendor"), sort, tie-out Σ vendors + unitemizedAmount === lineTotal, prefix-warning + empty-account throw.
- Spec 25: 15 in modelCoverageCommentaryShared.test.ts — prompt prints the aggregated rollup line with the correct vendors/contractors wording, omits it cleanly when absent; parseCommentaryBullets (well-formed / fenced / malformed-safe-degrade); fingerprintCommentary stability.
- Spec 26: +23 across 4 component test files (model-coverage-line-item-panel.test.tsx 14 new, model-coverage-commentary.test.tsx 9 new, plus band + table extensions), 45 total — HC vs non-HC leaf branch, terminal leaf (no further expand), loading/error/empty per group, source badge per source, fallbacks, bullet list, table click fires onSelectSchool, band commentary slot.
pnpm typecheck clean across all 5 workspaces; biome check clean on changed files.
### Self-review
No CRITICAL / security / Convex-correctness issues found:
- Both new live actions call requireSchoolPlAccess before any Redshift connection (per docs/endpoint-hardening.md); no new capability key (docs/capabilities.md).
- "use node" DB I/O goes through internalQuery / internalMutation (modelCoverageCommentaryStore.ts); the action has no direct ctx.db.
- Reducers and shared helpers are runtime-free (no Convex/Next/React runtime imports), per the architecture guardrails.
- Opus config correct (COMMENTARY_MODEL = "claude-opus-4-8", system + user prompt); missing key / Opus failure degrades to a "fallback" bullet rather than throwing.
- One MINOR comment fixed (commit 4189333a — corrected the max_tokens rationale comment).
### Resolved design decision
Concern B — network-wide commentary vs the scoped Miami + NY + Austin band. ✅ Resolved.
generateModelCoverageCommentary computes its digest network-wide (the action takes only { period }). That matches the unfiltered All Schools consolidated view, but would mismatch the AERIE-438 Miami + NY + Austin 3-school filter (AI prose describing the whole network beside a 3-school band).
Decision (requester): hide the AI commentary on the scoped 3-school view rather than re-scope the action. Commit de7b651f gates it via modelCommentaryEnabled = canQueryFinancials && schoolsAvmActive && isConsolidated && !consolidatedSchoolFilter — commentary is shown (and queried) only on the unfiltered All-Schools consolidated view, and is hidden (not queried; the band keeps its original single-row layout) under the Miami + NY + Austin filter. Two tests cover both states.
### Dependency chain
24 (vendor reducer+action) ─┐├─► 26 (drill-down panel + commentary UI)
25 (AI commentary action) ─┘
### Architecture notes
- Pure reducers stay runtime-free; live actions call requireSchoolPlAccess before opening any Redshift connection; cost-section gates reused so totals tie out to the P&L.
- New public Convex action follows docs/endpoint-hardening.md; cache store functions are internal* (implementation-only).
- modelCoverageCommentary cache is schema-defined (codegen only; no schema push).
- The frontend imports a runtime-free isContractedLaborAccount predicate from @bran/contracts/headcount-account-roles, not the backend isHeadcountAccount.
🤖 Generated with [Claude Code](https://claude.com/claude-code)