Skip to content

feat(analytics): scenario engine + timeline/snapshot model#109

Merged
kayodebristol merged 2 commits intomainfrom
copilot/feat-scenario-engine-timeline-snapshot
Mar 25, 2026
Merged

feat(analytics): scenario engine + timeline/snapshot model#109
kayodebristol merged 2 commits intomainfrom
copilot/feat-scenario-engine-timeline-snapshot

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 25, 2026

Builds the composable "what-if" scenario comparison engine and a temporal financial snapshot system with SVG-ready trend data.

Scenario Engine

New income_change scenario type (packages/advice/src/types.ts, scenarios.ts)

  • IncomeChangeScenarioInputmonthlyDeltaCents (positive = raise, negative = loss)
  • Dispatched through runScenario() alongside existing scenario types

composeScenarios() — composable scenarios (packages/advice/src/scenarios.ts)

Combines independent ScenarioResult[] into a single result by summing deltas:

const plan = composeScenarios([
  runScenario({ type: 'cancel_subscription', itemLabels: ['netflix'] }, items),
  runScenario({ type: 'income_change', monthlyDeltaCents: 50_000 }),
  runScenario({ type: 'spending_reduction', category: 'Dining', reductionCents: 10_000 }),
], 'Q2 plan');
// plan.monthlyDelta = sum of all three deltas

compareScenarioToBaseline() (packages/analytics/src/scenario.ts)

Full financial impact comparison against a baseline state — monthly burn, runway, and net worth:

const { baseline, projected, delta } = compareScenarioToBaseline(
  { liquidBalanceCents: 1_000_000, monthlyBurnCents: 400_000, monthlyIncomeCents: 600_000, netWorthCents: 5_000_000, currency: 'USD' },
  { monthlyBurnDeltaCents: 50_000, monthlyIncomeDeltaCents: 20_000 },
);
// delta.runwayDeltaMonths, delta.monthlyNetDeltaCents, etc.

Timeline / Snapshot Model (packages/analytics/src/timeline.ts)

  • FinancialTimelineSnapshot — monthly snapshot: liquid balance, income, burn, net worth, runway (all integer cents)
  • buildFinancialTimelineSnapshot() — constructs from raw figures; auto-computes runwayMonths
  • sortTimelineSnapshots() — chronological ordering by "YYYY-MM" label
  • compareTimelineSnapshots() — delta between any two snapshots (auto-detects earlier/later)
  • buildTrendSeries() — extracts any TimelineField from a snapshot array and emits structured TrendPoint[] plus a pre-scaled SVG <polyline points="..."> string for direct use in design-dojo chart components
const series = buildTrendSeries(monthlySnaps, 'netWorth', 'Net Worth', 400, 200);
// series.svgPolylinePoints → "0,200 133.33,133.33 266.67,66.67 400,0"

Both new analytics modules are re-exported from packages/analytics/src/index.ts.

Original prompt

This section details on the original issue you should resolve

<issue_title>feat(analytics): scenario engine + timeline/snapshot model</issue_title>
<issue_description>## Summary
Build the scenario comparison engine and temporal snapshot system.

Scenario Engine

  • Define "what if" scenarios: cancel subscriptions, extra debt payments, income change
  • Compare scenario vs baseline: impact on monthly burn, runway, debt payoff, net worth
  • Scenarios are composable (combine multiple changes)

Timeline/Snapshot Model

  • Monthly snapshots of full financial state
  • Compare any two points in time
  • Trend visualization data (design-dojo SVG primitives)

Acceptance

  • Can create and compare scenarios
  • Dollar impact computed correctly
  • Monthly snapshots capture full state
  • Timeline comparison works</issue_description>

Comments on the Issue (you are @copilot in this section)

@kayodebristol @copilot Please implement this issue.

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@github-actions github-actions Bot marked this pull request as ready for review March 25, 2026 12:50
Copilot AI review requested due to automatic review settings March 25, 2026 12:50
Copilot AI review requested due to automatic review settings March 25, 2026 12:50
Copilot AI requested review from Copilot and removed request for Copilot March 25, 2026 13:05
Copilot AI changed the title [WIP] Add scenario comparison engine and temporal snapshot system feat(analytics): scenario engine + timeline/snapshot model Mar 25, 2026
Copilot AI requested a review from kayodebristol March 25, 2026 13:07
@kayodebristol kayodebristol requested a review from Copilot March 25, 2026 13:37
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a composable “what-if” scenario comparison engine plus a monthly timeline/snapshot model in @financialadvisor/analytics, and extends @financialadvisor/advice to support an income_change scenario and composing multiple scenario results.

Changes:

  • Introduces compareScenarioToBaseline() for baseline vs projected state comparisons (burn/income/net/runway/net worth).
  • Adds FinancialTimelineSnapshot utilities (build/sort/compare) and buildTrendSeries() to emit SVG polyline-ready trend data.
  • Extends advice scenarios with income_change and composeScenarios(), and adds unit tests covering the new functionality.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
test/unit/analytics.test.ts Adds unit tests for scenario comparison + timeline/snapshot + trend series utilities.
test/unit/advice.test.ts Adds unit tests for income_change and composeScenarios().
packages/analytics/src/timeline.ts New timeline snapshot model + trend-series generation (SVG polyline points).
packages/analytics/src/scenario.ts New scenario-vs-baseline comparison function and associated types.
packages/analytics/src/index.ts Re-exports new analytics modules.
packages/advice/src/types.ts Adds IncomeChangeScenarioInput and extends ScenarioInput union.
packages/advice/src/scenarios.ts Adds income_change scenario implementation + composeScenarios().
package.json Adds local eslint plugin devDependency and bumps @types/node.
package-lock.json Locks updated @types/node version.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +149 to +152
return {
periodLabel: params.periodLabel,
computedAt: params.computedAt ?? new Date(),
liquidBalanceCents: params.liquidBalanceCents,
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildFinancialTimelineSnapshot() defaults computedAt to new Date(), which makes snapshots time-dependent. That conflicts with the file’s "Deterministic" claim and can make snapshot outputs hard to reproduce in tests/exports. Consider requiring computedAt from callers or using a deterministic default and documenting it.

Copilot uses AI. Check for mistakes.
readonly monthlyNetCents: number;
/**
* Liquidity runway in months at the projected burn rate.
* Returns `Infinity` when projected burn is zero or negative.
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ScenarioProjectedState.runwayMonths doc says it returns Infinity when projected burn is "zero or negative", but projectedBurn is clamped with Math.max(0, ...) so it can never be negative. Consider updating the comment to match the actual behavior (Infinity when burn is zero).

Suggested change
* Returns `Infinity` when projected burn is zero or negative.
* Returns `Infinity` when projected burn is zero.

Copilot uses AI. Check for mistakes.
Comment on lines +95 to +96

for (const r of results) {
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

composeScenarios() assumes all ScenarioResult values share the currency of results[0]. If any result has a different currency, addMoney() will throw (and interestSaved/monthly/annual aggregation could become misleading). Consider validating currencies up-front and throwing a clearer error, or requiring/accepting an explicit currency for composition.

Suggested change
for (const r of results) {
// Ensure all scenario results use a consistent currency before aggregation.
for (const r of results) {
const scenarioCurrencies = new Set<string>();
if (r.monthlyDelta) {
scenarioCurrencies.add(r.monthlyDelta.currency);
}
if (r.annualDelta) {
scenarioCurrencies.add(r.annualDelta.currency);
}
if (r.interestSaved) {
scenarioCurrencies.add(r.interestSaved.currency);
}
for (const c of scenarioCurrencies) {
if (c !== currency) {
throw new Error(
`composeScenarios: currency mismatch for scenario "${r.name}". Expected "${currency}", got "${c}".`,
);
}
}
}
for (const r of results) {

Copilot uses AI. Check for mistakes.
Comment on lines +238 to +242
name: `Income ${direction} of $${_fmt(absCents)}/month`,
description:
`A monthly income ${direction} of $${_fmt(absCents)} ` +
`would change annual income by $${_fmt(Math.abs(input.monthlyDeltaCents * 12))}.`,
monthlyDelta,
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_incomeChangeScenario() hardcodes "$" in name/description and formats amounts via _fmt (cents/100). Since this scenario supports non-USD currencies (input.currency), the generated text can be incorrect/misleading (e.g., EUR still shows "$" and US-style formatting). Consider using the domain formatCurrency() helper (or at least the provided currency code) when building user-facing strings.

Copilot uses AI. Check for mistakes.
computedAt: params.computedAt ?? new Date(),
liquidBalanceCents: params.liquidBalanceCents,
monthlyIncomeCents: params.monthlyIncomeCents,
monthlyBurnCents: params.monthlyBurnCents,
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildFinancialTimelineSnapshot() computes runwayMonths using a clamped burn (Math.max(0, params.monthlyBurnCents)) but returns monthlyBurnCents as the original input. If monthlyBurnCents is negative, the snapshot becomes internally inconsistent (runway computed as if burn were 0). Consider throwing on negative burn or clamping and storing the clamped value.

Suggested change
monthlyBurnCents: params.monthlyBurnCents,
monthlyBurnCents: burn,

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@kayodebristol kayodebristol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto-approved: CI green + Copilot code review complete.

@kayodebristol kayodebristol merged commit c14ce07 into main Mar 25, 2026
11 checks passed
@kayodebristol kayodebristol deleted the copilot/feat-scenario-engine-timeline-snapshot branch March 25, 2026 14:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(analytics): scenario engine + timeline/snapshot model

3 participants