Files
cve-dashboard/.kiro/specs/forecast-burndown-chart/design.md
Jordan Ramos a61d254ff9 Sync .kiro/ from master — v2.2.0 release batch
New specs: archer-template-library, ccp-metrics-view-restructure,
compliance-list-stale-after-sidebar-edit, compliance-metric-estimated-resolution-date,
compliance-remediation-display-fix, flexible-jira-ticket-creation,
forecast-burndown-chart, granite-loader-export, ivanti-queue-clear-completed-fix,
multi-item-jira-ticket, queue-collapsible-sections, vendor-issue-type-dropdown

New steering: archer-template-gen.md

Updated: migration-registration-check hook, remediation-plan-history spec,
gitlab-workflow, tech, versioning steering files
2026-06-04 11:27:31 -06:00

329 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)