Files
cve-dashboard/.kiro/specs/forecast-burndown-chart/design.md

329 lines
15 KiB
Markdown
Raw Normal View History

# Design Document: Forecast Burndown Chart
## Overview
This feature adds a per-metric forecast burndown chart to the CCP Metrics page. It combines historical compliance data (from `compliance_snapshots`) with forward-looking remediation projections (derived from `compliance_items` resolution dates) into a single stacked bar + line chart. A metric selector allows users to switch between individual compliance metrics (e.g., 2.3.5, 2.3.6, 5.2.5).
The design separates concerns into:
1. A **pure helper function** (`computeMetricForecastBurndown`) for testable forecast computation
2. Two **API endpoints** (forecast-burndown data + metrics-list) added to the existing VCL multi-vertical router
3. A **React chart component** using recharts `ComposedChart` with `Bar` + `Line` + `ReferenceLine`
Key design decisions:
- **No caching**: The API queries `compliance_items` directly on each request so edits on the AEO Compliance page are immediately reflected.
- **Pure computation**: The forecast logic is a stateless helper function that accepts pre-fetched data, making it testable in isolation without database mocks.
- **Existing patterns**: Routes use the `createVCLMultiVerticalRouter` factory pattern; the chart component follows the same inline-style + recharts patterns as `AggregatedBurndownChart` and `TrendChart`.
## Architecture
```mermaid
flowchart TD
subgraph Frontend
MS[MetricSelector] -->|selected metric_id| FBC[ForecastBurndownChart]
FBC -->|fetch| API1[GET /metric/:metricId/forecast-burndown]
MS -->|fetch on mount| API2[GET /metrics-list]
end
subgraph Backend
API1 --> RH1[Route Handler]
API2 --> RH2[Route Handler]
RH1 -->|query| DB[(PostgreSQL)]
RH1 -->|compute| HF[computeMetricForecastBurndown]
RH2 -->|query| DB
end
subgraph Database
DB --> CI[compliance_items]
DB --> CS[compliance_snapshots]
end
```
The data flow:
1. On page load, `MetricSelector` fetches `/api/compliance/vcl-multi/metrics-list` to populate the dropdown.
2. When a metric is selected, `ForecastBurndownChart` fetches `/api/compliance/vcl-multi/metric/:metricId/forecast-burndown`.
3. The route handler queries `compliance_items` for active devices and `compliance_snapshots` for historical data, then passes both to `computeMetricForecastBurndown`.
4. The helper returns structured data that the frontend renders as a `ComposedChart`.
## Components and Interfaces
### Backend: Helper Function
**File:** `backend/helpers/vclHelpers.js`
```javascript
/**
* Computes per-metric forecast burndown from device records and historical snapshots.
*
* @param {Array<{hostname: string, resolution_date: string|null}>} currentDevices
* Active non-compliant devices for the metric
* @param {number} totalAssets
* Total device count in scope for this metric (from snapshot or summary)
* @param {Array<{month: string, total_assets: number, non_compliant: number, compliance_pct: number}>} historicalSnapshots
* Pre-computed historical data points (up to 4 months)
* @returns {{
* historical: Array<{month: string, total_assets: number, non_compliant: number, compliance_pct: number}>,
* forecast: Array<{month: string, total_assets: number, non_compliant: number, compliance_pct: number}>,
* current_snapshot: {total_assets: number, non_compliant: number, compliant: number, compliance_pct: number, blockers: number, with_dates: number}
* }}
*/
function computeMetricForecastBurndown(currentDevices, totalAssets, historicalSnapshots) { ... }
```
### Backend: API Endpoints
**File:** `backend/routes/vclMultiVertical.js` (added to existing router)
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/api/compliance/vcl-multi/metric/:metricId/forecast-burndown` | `requireAuth()` | Returns combined historical + forecast data for a metric |
| GET | `/api/compliance/vcl-multi/metrics-list` | `requireAuth()` | Returns list of metrics with active non-compliant device counts |
**Forecast Burndown Response Shape:**
```json
{
"metric_id": "2.3.5",
"historical": [
{ "month": "2025-01", "total_assets": 500, "non_compliant": 45, "compliance_pct": 91.0 },
{ "month": "2025-02", "total_assets": 510, "non_compliant": 38, "compliance_pct": 92.5 }
],
"forecast": [
{ "month": "2025-04", "total_assets": 520, "non_compliant": 25, "compliance_pct": 95.2 },
{ "month": "2025-05", "total_assets": 520, "non_compliant": 15, "compliance_pct": 97.1 }
],
"current_snapshot": {
"total_assets": 520,
"non_compliant": 30,
"compliant": 490,
"compliance_pct": 94.2,
"blockers": 8,
"with_dates": 22
}
}
```
**Metrics List Response Shape:**
```json
[
{ "metric_id": "2.3.5", "device_count": 30 },
{ "metric_id": "2.3.6", "device_count": 12 },
{ "metric_id": "5.2.5", "device_count": 45 }
]
```
### Frontend: Components
**File:** `frontend/src/components/pages/CCPMetricsPage.js` (added to existing file)
1. **`MetricSelector`** — Dropdown component that fetches and displays available metrics. Triggers forecast data fetch on selection change.
2. **`ForecastBurndownChart`** — Renders a `ComposedChart` with:
- Blue `Bar` for `total_assets` (left Y-axis)
- Orange `Bar` for `non_compliant` (left Y-axis, stacked appearance)
- Green `Line` for `compliance_pct` (right Y-axis, 0100%)
- `ReferenceLine` as vertical divider between historical and forecast sections
- Forecast data points rendered at 50% opacity
## Data Models
### Database Tables (existing, no schema changes required)
**`compliance_items`** (relevant columns):
| Column | Type | Description |
|--------|------|-------------|
| hostname | TEXT | Device hostname |
| metric_id | TEXT | Compliance metric identifier (e.g., "2.3.5") |
| status | TEXT | `'active'` or `'resolved'` |
| resolution_date | DATE | Planned remediation date (nullable) |
| vertical | TEXT | Organizational vertical (e.g., "NTS_AEO") |
| team | TEXT | Team assignment |
**`compliance_snapshots`** (relevant columns):
| Column | Type | Description |
|--------|------|-------------|
| snapshot_month | TEXT | Month in YYYY-MM format |
| vertical | TEXT | Organizational vertical |
| total_devices | INTEGER | Total devices in scope |
| compliant | INTEGER | Compliant device count |
| non_compliant | INTEGER | Non-compliant device count |
| compliance_pct | NUMERIC(5,2) | Compliance percentage |
### Internal Data Structures
**Chart data point (shared between historical and forecast):**
```javascript
{
month: "2025-04", // YYYY-MM
total_assets: 520, // Total devices in scope
non_compliant: 25, // Non-compliant count
compliance_pct: 95.2, // Percentage (0-100, 1 decimal)
isForecast: true // Frontend flag for opacity styling
}
```
**Current snapshot:**
```javascript
{
total_assets: 520,
non_compliant: 30,
compliant: 490,
compliance_pct: 94.2,
blockers: 8, // Devices with no resolution_date
with_dates: 22 // Devices with a resolution_date
}
```
### Query Patterns
**Metrics list query:**
```sql
SELECT metric_id, COUNT(DISTINCT hostname) AS device_count
FROM compliance_items
WHERE status = 'active' AND vertical IS NOT NULL
GROUP BY metric_id
ORDER BY metric_id ASC
```
**Active devices for a metric:**
```sql
SELECT hostname, resolution_date, vertical
FROM compliance_items
WHERE metric_id = $1 AND status = 'active' AND vertical IS NOT NULL
```
**Historical snapshots for a vertical (3 months prior):**
```sql
SELECT snapshot_month AS month, total_devices AS total_assets,
non_compliant, compliance_pct::numeric AS compliance_pct
FROM compliance_snapshots
WHERE vertical = $1 AND snapshot_month >= $2 AND snapshot_month < $3
ORDER BY snapshot_month ASC
```
## Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
The `computeMetricForecastBurndown` helper function is a pure function with clear input/output behavior and universal properties that hold across a wide input space. This makes it an ideal candidate for property-based testing.
### Property 1: Forecast structure invariant
*For any* valid `currentDevices` array and non-negative `totalAssets` integer, the `computeMetricForecastBurndown` function SHALL return an object with `historical`, `forecast`, and `current_snapshot` fields, where every element in the `forecast` array contains `month` (YYYY-MM string), `total_assets` (equal to the input `totalAssets` or the current snapshot's total), `non_compliant` (non-negative integer), and `compliance_pct` (number between 0 and 100), and all `total_assets` values in the forecast array are identical.
**Validates: Requirements 1.4, 3.1, 3.6**
### Property 2: Blocker and with_dates partition invariant
*For any* valid `currentDevices` array and non-negative `totalAssets` integer, the `computeMetricForecastBurndown` function SHALL produce a `current_snapshot` where `blockers + with_dates = non_compliant`.
**Validates: Requirements 3.2**
### Property 3: Compliance percentage formula correctness
*For any* valid inputs where `totalAssets > 0`, the `computeMetricForecastBurndown` function SHALL compute `compliance_pct` for each forecast and current_snapshot data point as `ROUND((total_assets - non_compliant) / total_assets * 100, 1)`. When `totalAssets` is 0, all `compliance_pct` values SHALL be 0.
**Validates: Requirements 3.3, 3.10**
### Property 4: Forecast non_compliant monotonicity
*For any* valid `currentDevices` array and non-negative `totalAssets` integer, the `forecast` array produced by `computeMetricForecastBurndown` SHALL have monotonically non-increasing `non_compliant` values (each month's `non_compliant` is less than or equal to the previous month's `non_compliant`).
**Validates: Requirements 3.4**
### Property 5: Per-month non_compliant computation correctness
*For any* valid `currentDevices` array and non-negative `totalAssets` integer, for each month in the `forecast` array, the `non_compliant` value SHALL equal the count of devices whose `resolution_date` is after that month (not yet remediated) plus the count of devices with no `resolution_date` (blockers).
**Validates: Requirements 3.8**
### Property 6: Forecast horizon bound
*For any* valid `currentDevices` array and non-negative `totalAssets` integer, the `forecast` array produced by `computeMetricForecastBurndown` SHALL contain at most 12 elements, and SHALL terminate either when all devices with resolution dates are projected to be remediated or at the 12-month maximum, whichever comes first.
**Validates: Requirements 1.9, 3.9**
### Property 7: Past-due resolution dates treated as current month
*For any* device in `currentDevices` whose `resolution_date` is in a month that has already passed relative to the current date, the `computeMetricForecastBurndown` function SHALL treat that device as projected to be remediated in the current month (i.e., it is excluded from `non_compliant` in the first forecast month if that month is after the current month, but remains in `non_compliant` for the current month's snapshot).
**Validates: Requirements 6.5**
## Error Handling
### Backend API Errors
| Scenario | HTTP Status | Response Body | Behavior |
|----------|-------------|---------------|----------|
| Unauthenticated request | 401 | `{ "error": "Unauthorized" }` | `requireAuth()` middleware rejects |
| Invalid/unknown metricId | 200 | `{ metric_id, historical: [], forecast: [], current_snapshot: {zeros} }` | Graceful empty response |
| Database query failure | 500 | `{ "error": "Failed to compute forecast burndown" }` | Catch block logs error, returns 500 |
| Database failure on metrics-list | 500 | `{ "error": "Failed to fetch metrics list" }` | Catch block logs error, returns 500 |
### Frontend Error States
| Scenario | UI Behavior |
|----------|-------------|
| Metrics list fetch fails | Inline error with AlertCircle icon, red border, error message text |
| Forecast data fetch fails | Inline error in chart area with AlertCircle icon and error description |
| Empty metrics list | "No metrics with active non-compliant devices" message |
| Empty forecast + empty historical | "No data available for this metric" message in chart area |
| Race condition (rapid metric switching) | Discard stale responses; only render data from the most recent selection |
### Helper Function Edge Cases
| Input Condition | Behavior |
|-----------------|----------|
| `currentDevices` is empty | Return empty `forecast`, zeroed `current_snapshot` (except `total_assets`) |
| `totalAssets` is 0 | All `compliance_pct` values are 0 (avoid division by zero) |
| `totalAssets` < `currentDevices.length` | Use `currentDevices.length` as `non_compliant` without clamping |
| All devices are blockers (no resolution dates) | Return empty `forecast` array (no month-by-month projection possible) |
| All resolution dates are in the past | Treat as remediated in current month; forecast shows only blockers remaining |
## Testing Strategy
### Property-Based Tests (via fast-check)
The `computeMetricForecastBurndown` helper function is a pure function with well-defined invariants, making it ideal for property-based testing. Each property test runs a minimum of 100 iterations with randomly generated inputs.
**Library:** `fast-check` (already available in the project's test dependencies based on existing `.property.test.js` files)
**Test file:** `backend/__tests__/forecast-burndown-chart.property.test.js`
**Generator strategy:**
- `currentDevices`: Array of `{ hostname: arbitraryString, resolution_date: oneOf(null, arbitraryFutureDate, arbitraryPastDate) }`
- `totalAssets`: Non-negative integer (including 0 and values less than device count)
- `historicalSnapshots`: Array of 04 elements with valid month strings and non-negative counts
Each test is tagged with: `Feature: forecast-burndown-chart, Property {N}: {title}`
Properties to implement:
1. Forecast structure invariant
2. Blocker/with_dates partition invariant
3. Compliance percentage formula correctness
4. Forecast non_compliant monotonicity
5. Per-month non_compliant computation correctness
6. Forecast horizon bound
7. Past-due resolution dates treated as current month
### Unit Tests (example-based)
**Test file:** `backend/__tests__/forecast-burndown-chart.test.js`
- Specific examples with known inputs and expected outputs
- Edge cases: empty devices, zero totalAssets, all blockers, all past-due dates
- Integration with route handlers (mocked database)
### Integration Tests
- Verify API endpoints return correct response shapes with seeded database data
- Verify authentication middleware is applied
- Verify correct SQL filtering (vertical scoping, active-only devices)
- Verify historical data derivation from `compliance_snapshots`
### Frontend Tests
- Component rendering with mock data (snapshot tests)
- Loading and error states
- Metric selector interaction (selection triggers fetch)
- Race condition handling (rapid metric switching discards stale responses)