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

15 KiB
Raw Blame 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

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

/**
 * 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:

{
  "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:

[
  { "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):

{
  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:

{
  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:

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:

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):

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)