Update .kiro: remove SQLite hooks, add PostgreSQL migration hook, add workflow steering, sync specs

This commit is contained in:
Jordan Ramos
2026-05-12 14:45:58 -06:00
parent 3ee8487286
commit 1bb8ec1658
35 changed files with 4645 additions and 48 deletions

View File

@@ -0,0 +1 @@
{"specId": "9f706e1f-2c1d-4eb0-aa80-b511a0be2bd3", "workflowType": "requirements-first", "specType": "feature"}

View File

@@ -0,0 +1,313 @@
# Design Document: Compliance Chart Replacements
## Overview
This feature replaces two underperforming charts in the `ComplianceChartsPanel` with more actionable visualizations:
| Slot | Old Chart | New Chart |
|------|-----------|-----------|
| Chart 4 | MTTR by Team (horizontal bar) | **Aging Findings Distribution** — stacked vertical bar showing active findings bucketed by `seen_count` with per-team color segments |
| Chart 5 | Most Persistent Findings (horizontal bar) | **Net Change Waterfall** — waterfall bar chart showing per-cycle net movement (start → +new → +recurring → resolved → end) |
Both new charts derive data from columns already present in the SQLite schema (`compliance_items.seen_count`, `compliance_uploads.new_count / recurring_count / resolved_count`). No database migrations are required. The four other charts (Active Findings Over Time, Change per Cycle, Team Compliance Health, Archer Exception Pipeline) remain unchanged.
The backend reuses the existing route paths (`/mttr` → aging, `/top-recurring` → waterfall) so no new API routes are introduced. The frontend replaces the two component functions and their corresponding state variables while preserving the shared infrastructure (`ChartCard`, `DarkTooltip`, `NoData`, `TEAM_COLORS`, `fmtDate`, style tokens).
## Architecture
The change is scoped to two files and follows the existing vertical-slice pattern:
```mermaid
graph LR
subgraph Backend ["Express Backend"]
R1["/compliance/mttr<br/>(was MTTR → now Aging)"]
R2["/compliance/top-recurring<br/>(was Persistent → now Waterfall)"]
DB[(SQLite<br/>compliance_items<br/>compliance_uploads)]
R1 -->|"SELECT … GROUP BY bucket, team"| DB
R2 -->|"SELECT … ORDER BY report_date"| DB
end
subgraph Frontend ["React Frontend"]
Panel["ComplianceChartsPanel"]
AC["AgingChart"]
WC["WaterfallChart"]
Panel --> AC
Panel --> WC
end
R1 -->|JSON| AC
R2 -->|JSON| WC
```
### Key Architectural Decisions
1. **Route path reuse** — The old `/mttr` and `/top-recurring` paths are replaced in-place rather than creating new paths. This avoids route proliferation and means no proxy/CORS changes are needed.
2. **No new dependencies** — Both charts are built with the existing Recharts `BarChart` / `Bar` components. The waterfall effect is achieved using a transparent "base" `Bar` with `fill="transparent"` to offset the visible segments, which is a standard Recharts stacking technique.
3. **No database migration** — The aging chart queries `compliance_items.seen_count` (already populated by the upload pipeline). The waterfall chart reads `new_count`, `recurring_count`, `resolved_count` from `compliance_uploads` (already written by `persistUpload`). Starting/ending counts are computed in the route handler via a running accumulator.
4. **Shared component reuse** — Both new chart components use the existing `ChartCard`, `DarkTooltip`, `NoData`, `TEAM_COLORS`, `AXIS_STYLE`, `GRID_STYLE`, and `LEGEND_STYLE` tokens already defined in `ComplianceChartsPanel.js`.
## Components and Interfaces
### Backend: Aging Endpoint (replaces `/mttr`)
**Route:** `GET /compliance/mttr`
**SQL Query:**
```sql
SELECT
CASE
WHEN seen_count = 1 THEN '1 cycle'
WHEN seen_count BETWEEN 2 AND 3 THEN '23 cycles'
WHEN seen_count BETWEEN 4 AND 6 THEN '46 cycles'
ELSE '7+ cycles'
END AS bucket,
team,
COUNT(*) AS count
FROM compliance_items
WHERE status = 'active'
GROUP BY bucket, team
ORDER BY
CASE bucket
WHEN '1 cycle' THEN 1
WHEN '23 cycles' THEN 2
WHEN '46 cycles' THEN 3
WHEN '7+ cycles' THEN 4
END
```
**Response shape:**
```json
{
"aging": [
{ "bucket": "1 cycle", "total": 12, "STEAM": 5, "ACCESS-ENG": 3, "ACCESS-OPS": 2, "INTELDEV": 2 },
{ "bucket": "23 cycles", "total": 8, "STEAM": 2, "ACCESS-ENG": 4, "ACCESS-OPS": 1, "INTELDEV": 1 },
{ "bucket": "46 cycles", "total": 3, "STEAM": 1, "ACCESS-ENG": 0, "ACCESS-OPS": 2, "INTELDEV": 0 },
{ "bucket": "7+ cycles", "total": 2, "STEAM": 0, "ACCESS-ENG": 1, "ACCESS-OPS": 0, "INTELDEV": 1 }
]
}
```
**Handler logic:**
1. Query active items grouped by bucket CASE expression and team.
2. Pivot the flat rows into one object per bucket with per-team counts and a total.
3. Return the array sorted by bucket order (ascending age).
### Backend: Waterfall Endpoint (replaces `/top-recurring`)
**Route:** `GET /compliance/top-recurring`
**SQL Query:**
```sql
SELECT id, report_date,
COALESCE(new_count, 0) AS new_count,
COALESCE(recurring_count, 0) AS recurring_count,
COALESCE(resolved_count, 0) AS resolved_count
FROM compliance_uploads
ORDER BY report_date ASC
```
**Response shape:**
```json
{
"waterfall": [
{
"date": "2026-04-13",
"start": 0,
"new_count": 15,
"recurring_count": 0,
"resolved_count": 0,
"end": 15
},
{
"date": "2026-04-20",
"start": 15,
"new_count": 3,
"recurring_count": 12,
"resolved_count": 5,
"end": 25
}
]
}
```
**Handler logic:**
1. Fetch all uploads ordered by `report_date ASC`.
2. Iterate with a running accumulator: `start` for cycle N = `end` of cycle N1 (first cycle starts at 0).
3. Compute `end = start + new_count + recurring_count - resolved_count`.
4. Return the array.
### Frontend: `AgingChart` Component
Replaces `MttrChart`. Renders a vertical stacked `BarChart`:
- **X-axis:** bucket labels (`1 cycle`, `23 cycles`, `46 cycles`, `7+ cycles`)
- **Y-axis:** finding count
- **Stacked bars:** one `<Bar>` per team key from `TEAM_COLORS`, using `stackId="aging"`
- **Tooltip:** `DarkTooltip` showing bucket label, per-team counts, and total
- **Empty state:** `<NoData />` when `aging` array is empty
- **Wrapper:** `<ChartCard title="Aging Findings Distribution" subtitle="Active findings by age bucket — stacked by team" />`
### Frontend: `WaterfallChart` Component
Replaces `RecurringChart`. Renders a vertical `BarChart` with stacked transparent base:
- **X-axis:** cycle dates formatted via `fmtDate`
- **Y-axis:** finding count
- **Bars (stacked):**
1. `<Bar dataKey="start" stackId="w" fill="transparent" />` — invisible base
2. `<Bar dataKey="new_count" stackId="w" fill="#EF4444" />` — red new findings
3. `<Bar dataKey="recurring_count" stackId="w" fill="#F59E0B" />` — amber recurring
- **Separate bar:** `<Bar dataKey="resolved_count" fill="#10B981" />` rendered as a standalone (not stacked) to visually represent the downward resolution. Since Recharts doesn't natively support negative waterfall segments, the resolved bar is rendered separately alongside the stacked group, matching the existing `DeltaChart` pattern already used in Chart 2.
- **Tooltip:** Custom `WaterfallTooltip` (extends `DarkTooltip` pattern) showing date, start, new, recurring, resolved, and end values.
- **Empty state:** `<NoData />` when `waterfall` array is empty
- **Wrapper:** `<ChartCard title="Net Change Waterfall" subtitle="Per-cycle net movement: start → +new → +recurring → resolved → end" />`
### Frontend: State & Fetch Changes in `ComplianceChartsPanel`
| Old | New |
|-----|-----|
| `const [mttr, setMttr] = useState([])` | `const [aging, setAging] = useState([])` |
| `const [recurring, setRecurring] = useState([])` | `const [waterfall, setWaterfall] = useState([])` |
| `fetch(…/compliance/mttr)``setMttr(d.mttr)` | `fetch(…/compliance/mttr)``setAging(d.aging)` |
| `fetch(…/compliance/top-recurring)``setRecurring(d.items)` | `fetch(…/compliance/top-recurring)``setWaterfall(d.waterfall)` |
The `Promise.all` pattern and error handling remain identical. The other two fetches (`/trends`, `/archer-tickets/status-trend`) are untouched.
### Code Removal
- Delete `MttrChart` function component
- Delete `RecurringChart` function component
- Remove `mttr` and `recurring` state variables and their setter calls
- Remove old `/mttr` handler SQL and logic in `compliance.js`
- Remove old `/top-recurring` handler SQL and logic in `compliance.js`
## Data Models
No schema changes are required. The feature reads from existing columns:
### `compliance_items` (read by Aging endpoint)
| Column | Type | Usage |
|--------|------|-------|
| `status` | TEXT (`'active'` / `'resolved'`) | Filter to active items only |
| `seen_count` | INTEGER (default 1) | Bucketed into age groups via CASE expression |
| `team` | TEXT | Group-by for per-team stacked segments |
### `compliance_uploads` (read by Waterfall endpoint)
| Column | Type | Usage |
|--------|------|-------|
| `report_date` | TEXT (YYYY-MM-DD) | X-axis label, ordering |
| `new_count` | INTEGER (default 0) | New findings in this cycle |
| `recurring_count` | INTEGER (default 0) | Recurring findings in this cycle |
| `resolved_count` | INTEGER (default 0) | Resolved findings in this cycle |
### Derived Waterfall Fields (computed in handler, not stored)
| Field | Computation |
|-------|-------------|
| `start` | Previous cycle's `end` (0 for first cycle) |
| `end` | `start + new_count + recurring_count - resolved_count` |
## 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.*
### Property 1: Aging bucketing correctness and conservation
*For any* array of active compliance items with arbitrary `seen_count` values (≥ 1) and arbitrary team assignments, applying the aging bucketing logic SHALL:
- assign every item to exactly one of the four buckets (`1 cycle` for seen_count = 1, `23 cycles` for 2 ≤ seen_count ≤ 3, `46 cycles` for 4 ≤ seen_count ≤ 6, `7+ cycles` for seen_count ≥ 7),
- produce per-team counts within each bucket that sum to the bucket's total count, and
- produce bucket totals that sum to the total number of input items.
**Validates: Requirements 1.1, 1.2**
### Property 2: Waterfall chain linkage and arithmetic invariant
*For any* ordered sequence of upload records with non-negative `new_count`, `recurring_count`, and `resolved_count` values, computing the waterfall SHALL produce a result where:
- the first row has `start = 0`,
- for every subsequent row N, `start[N] = end[N-1]` (chain linkage), and
- for every row, `end = start + new_count + recurring_count - resolved_count` (arithmetic invariant).
**Validates: Requirements 3.1, 3.2, 3.3**
## Error Handling
### Backend
| Scenario | Behavior |
|----------|----------|
| No active findings (aging endpoint) | Return `{ aging: [] }` with HTTP 200 |
| No uploads (waterfall endpoint) | Return `{ waterfall: [] }` with HTTP 200 |
| SQLite query error (either endpoint) | Log error to console, return HTTP 500 with `{ error: "Database error" }` |
| `seen_count` is NULL or missing | `COALESCE(seen_count, 1)` in the CASE expression treats NULL as 1 cycle |
| `new_count` / `recurring_count` / `resolved_count` is NULL | `COALESCE(..., 0)` in the SELECT ensures zero-default (already present in the uploads table pattern) |
### Frontend
| Scenario | Behavior |
|----------|----------|
| Aging fetch fails (network error or non-200) | `aging` state remains `[]`, `AgingChart` renders `<NoData />` |
| Waterfall fetch fails (network error or non-200) | `waterfall` state remains `[]`, `WaterfallChart` renders `<NoData />` |
| One fetch fails, others succeed | Unaffected charts render normally — each fetch result is handled independently in the existing `try/catch` pattern |
| API returns unexpected shape | Component guards (`data.length === 0`) trigger `<NoData />` gracefully |
## Testing Strategy
### Property-Based Tests (fast-check)
The project backend already uses Jest. Property-based tests will use [fast-check](https://github.com/dubzzz/fast-check) (already available in the Node ecosystem, zero-config with Jest).
Each property test runs a minimum of **100 iterations** with randomly generated inputs.
**Test 1: Aging bucketing correctness and conservation**
- **Tag:** `Feature: compliance-chart-replacements, Property 1: Aging bucketing correctness and conservation`
- **Generator:** Array of objects `{ seen_count: fc.integer({ min: 1, max: 200 }), team: fc.constantFrom('STEAM', 'ACCESS-ENG', 'ACCESS-OPS', 'INTELDEV') }`
- **Assertion:** Extract the pure bucketing/pivoting function from the route handler. For each generated array, verify:
1. Every item maps to exactly one of the four bucket labels
2. Per-team counts in each bucket sum to the bucket total
3. All bucket totals sum to the input array length
**Test 2: Waterfall chain linkage and arithmetic invariant**
- **Tag:** `Feature: compliance-chart-replacements, Property 2: Waterfall chain linkage and arithmetic invariant`
- **Generator:** Array of objects `{ new_count: fc.nat({ max: 50 }), recurring_count: fc.nat({ max: 50 }), resolved_count: fc.nat({ max: 50 }), report_date: fc.date().map(d => d.toISOString().slice(0, 10)) }`
- **Assertion:** Extract the pure waterfall computation function. For each generated sequence, verify:
1. `result[0].start === 0`
2. For all i > 0: `result[i].start === result[i-1].end`
3. For all i: `result[i].end === result[i].start + result[i].new_count + result[i].recurring_count - result[i].resolved_count`
### Unit Tests (Jest, example-based)
| Test | Validates |
|------|-----------|
| Aging endpoint returns correct JSON shape with known seed data | Req 1.3 |
| Aging endpoint returns empty array when no active items exist | Req 1.4 |
| Aging endpoint returns 500 on DB error | Req 1.5 |
| Waterfall endpoint returns empty array when no uploads exist | Req 3.5 |
| Waterfall endpoint returns 500 on DB error | Req 3.6 |
| `AgingChart` renders `<NoData />` for empty data | Req 2.5 |
| `AgingChart` renders ChartCard with correct title | Req 2.3 |
| `WaterfallChart` renders `<NoData />` for empty data | Req 4.5 |
| `WaterfallChart` renders ChartCard with correct title | Req 4.6 |
### Integration Tests
| Test | Validates |
|------|-----------|
| Dashboard fetches `/compliance/mttr` and `/compliance/top-recurring` on mount | Req 5.1, 5.2 |
| Dashboard still fetches `/compliance/trends` and `/archer-tickets/status-trend` | Req 5.3 |
| All four fetches run in parallel via `Promise.all` | Req 5.4 |
| Single fetch failure does not break other charts | Req 5.5 |
### Smoke Tests
| Test | Validates |
|------|-----------|
| `/compliance/mttr` route responds and does NOT return old `{ mttr: [...] }` shape | Req 1.6 |
| `/compliance/top-recurring` route responds and does NOT return old `{ items: [...] }` shape | Req 3.7 |
### Testability Design Note
To enable property-based testing, the bucketing logic and waterfall accumulator will be extracted as **pure exported functions** (e.g., `bucketAgingItems(items)` and `computeWaterfall(uploads)`) separate from the Express route handlers. This allows the property tests to call the pure functions directly without needing HTTP or database mocking.

View File

@@ -0,0 +1,99 @@
# Requirements Document
## Introduction
Replace two underperforming compliance dashboard charts with more actionable visualizations. Chart 4 (MTTR by Team) is replaced by an Aging Findings Distribution histogram, and Chart 5 (Most Persistent Findings) is replaced by a Net Change Waterfall chart. Both replacements use data already present in the existing SQLite schema (`compliance_items.seen_count` and `compliance_uploads` aggregate columns) and require no database migrations. The four other charts in `ComplianceChartsPanel` remain unchanged.
## Glossary
- **Dashboard**: The compliance charts panel rendered by `ComplianceChartsPanel.js`, containing six chart cards in a responsive grid.
- **Aging_Chart**: The new Chart 4 replacement — a stacked bar / histogram showing active findings distributed across age buckets derived from `seen_count`.
- **Waterfall_Chart**: The new Chart 5 replacement — a waterfall chart showing net movement per upload cycle (starting count → +new → +recurring → resolved → ending count).
- **Cycle**: A single compliance report upload. Each upload represents one weekly reporting period.
- **Age_Bucket**: A grouping of active findings by how many cycles they have been continuously observed: 1 cycle, 23 cycles, 46 cycles, 7+ cycles.
- **Seen_Count**: The `seen_count` column on `compliance_items`, incremented each cycle an active finding persists. Starts at 1 on first appearance.
- **Compliance_API**: The Express router at `cve-dashboard/backend/routes/compliance.js` serving `/api/compliance/*` endpoints.
- **ChartCard**: The shared wrapper component providing title, subtitle, and dark-themed card styling for each chart in the Dashboard.
- **Recharts**: The React charting library used by the Dashboard for all visualizations.
- **Design_System**: The dark theme specification defined in `DESIGN_SYSTEM.md`, including color palette, monospace fonts, and card styling.
## Requirements
### Requirement 1: Aging Findings Distribution Backend Endpoint
**User Story:** As a compliance analyst, I want an API endpoint that returns active findings grouped by age bucket, so that the frontend can render the Aging Findings Distribution chart.
#### Acceptance Criteria
1. WHEN a GET request is made to the aging endpoint, THE Compliance_API SHALL return active findings from `compliance_items` grouped into four Age_Buckets: 1 cycle (`seen_count = 1`), 23 cycles (`seen_count` between 2 and 3), 46 cycles (`seen_count` between 4 and 6), and 7+ cycles (`seen_count >= 7`).
2. WHEN a GET request is made to the aging endpoint, THE Compliance_API SHALL include a per-team breakdown within each Age_Bucket so the frontend can render stacked segments.
3. THE Compliance_API SHALL return the response as a JSON object with an array of bucket objects, each containing the bucket label, total count, and a count per team.
4. WHEN no active findings exist, THE Compliance_API SHALL return an empty array with HTTP status 200.
5. IF a database error occurs, THEN THE Compliance_API SHALL return HTTP status 500 with a JSON error message.
6. THE Compliance_API SHALL replace the existing `/mttr` endpoint path so that no new route paths are introduced and the old MTTR endpoint is removed.
### Requirement 2: Aging Findings Distribution Frontend Chart
**User Story:** As a compliance analyst, I want to see a stacked bar chart showing how many active findings fall into each age bucket, so that I can understand the shape of the current backlog and identify stale findings.
#### Acceptance Criteria
1. THE Aging_Chart SHALL render a vertical stacked bar chart using Recharts with one bar group per Age_Bucket (1 cycle, 23 cycles, 46 cycles, 7+ cycles) on the X-axis and finding count on the Y-axis.
2. THE Aging_Chart SHALL use one stacked color segment per team, using the existing `TEAM_COLORS` mapping defined in the Dashboard.
3. THE Aging_Chart SHALL be wrapped in a ChartCard with the title "Aging Findings Distribution" and a subtitle describing the chart purpose.
4. THE Aging_Chart SHALL use the shared `DarkTooltip` component to display bucket label, per-team counts, and total on hover.
5. THE Aging_Chart SHALL render a `NoData` placeholder when the API returns an empty array.
6. THE Aging_Chart SHALL occupy the same grid position (Chart 4 slot) previously held by the MTTR chart in the Dashboard layout.
7. THE Aging_Chart SHALL conform to the Design_System by using monospace axis labels, the standard grid style, and the dark card background gradient.
### Requirement 3: Net Change Waterfall Backend Endpoint
**User Story:** As a compliance analyst, I want an API endpoint that returns per-cycle waterfall data (starting count, new, recurring, resolved, ending count), so that the frontend can render the Net Change Waterfall chart.
#### Acceptance Criteria
1. WHEN a GET request is made to the waterfall endpoint, THE Compliance_API SHALL return one row per upload cycle containing: the cycle report date, starting active count, new count, recurring count, resolved count, and ending active count.
2. THE Compliance_API SHALL compute the starting count for each cycle as the ending count of the previous cycle, with the first cycle starting at zero.
3. THE Compliance_API SHALL compute the ending count for each cycle as `starting_count + new_count + recurring_count - resolved_count`.
4. THE Compliance_API SHALL read `new_count`, `recurring_count`, and `resolved_count` directly from the `compliance_uploads` table without requiring additional aggregation queries.
5. WHEN no uploads exist, THE Compliance_API SHALL return an empty array with HTTP status 200.
6. IF a database error occurs, THEN THE Compliance_API SHALL return HTTP status 500 with a JSON error message.
7. THE Compliance_API SHALL replace the existing `/top-recurring` endpoint path so that no new route paths are introduced and the old persistent-findings endpoint is removed.
### Requirement 4: Net Change Waterfall Frontend Chart
**User Story:** As a compliance analyst, I want to see a waterfall chart showing net movement per cycle (starting count → +new → +recurring → resolved → ending count), so that I can immediately tell whether the compliance posture is improving or degrading.
#### Acceptance Criteria
1. THE Waterfall_Chart SHALL render a bar chart using Recharts where each cycle shows four visual segments: a transparent base (starting count), a red segment for new findings, an amber segment for recurring findings, and a green downward segment for resolved findings, producing a waterfall effect.
2. WHEN a user hovers over any segment, THE Waterfall_Chart SHALL display a DarkTooltip showing the cycle date, starting count, new count, recurring count, resolved count, and ending count.
3. THE Waterfall_Chart SHALL label the X-axis with cycle dates formatted as MM/DD/YY using the existing `fmtDate` helper.
4. THE Waterfall_Chart SHALL label the Y-axis with finding counts using monospace axis styling consistent with the Design_System.
5. THE Waterfall_Chart SHALL render a `NoData` placeholder when the API returns an empty array.
6. THE Waterfall_Chart SHALL be wrapped in a ChartCard with the title "Net Change Waterfall" and a subtitle describing the chart purpose.
7. THE Waterfall_Chart SHALL occupy the same grid position (Chart 5 slot) previously held by the Most Persistent Findings chart in the Dashboard layout.
8. THE Waterfall_Chart SHALL use `#EF4444` for new findings, `#F59E0B` for recurring findings, and `#10B981` for resolved findings, matching the existing color conventions in the Dashboard.
### Requirement 5: Frontend Data Fetching Integration
**User Story:** As a developer, I want the Dashboard to fetch data from the two new endpoints in place of the old ones, so that the panel loads the correct data for the replacement charts without breaking the other four charts.
#### Acceptance Criteria
1. THE Dashboard SHALL replace the fetch call to `/compliance/mttr` with a fetch call to the aging endpoint and store the response in component state.
2. THE Dashboard SHALL replace the fetch call to `/compliance/top-recurring` with a fetch call to the waterfall endpoint and store the response in component state.
3. THE Dashboard SHALL continue to fetch `/compliance/trends` and `/archer-tickets/status-trend` without modification.
4. THE Dashboard SHALL load all four endpoints in parallel using `Promise.all` on component mount, matching the existing loading pattern.
5. IF any individual fetch fails, THEN THE Dashboard SHALL render the corresponding chart in its `NoData` state without affecting the other charts.
### Requirement 6: Removal of Obsolete Code
**User Story:** As a developer, I want the old MTTR and Persistent Findings chart components and endpoints removed, so that the codebase does not contain dead code.
#### Acceptance Criteria
1. THE Dashboard SHALL remove the `MttrChart` component function and all references to the `mttr` state variable.
2. THE Dashboard SHALL remove the `RecurringChart` component function and all references to the `recurring` state variable.
3. THE Compliance_API SHALL remove the old `/mttr` SQL query and handler logic, replacing it with the aging endpoint handler at the same route path.
4. THE Compliance_API SHALL remove the old `/top-recurring` SQL query and handler logic, replacing it with the waterfall endpoint handler at the same route path.

View File

@@ -0,0 +1,140 @@
# Implementation Plan: Compliance Chart Replacements
## Overview
Replace Chart 4 (MTTR by Team) and Chart 5 (Most Persistent Findings) in the compliance dashboard with two new visualizations: Aging Findings Distribution (stacked bar by age bucket) and Net Change Waterfall (per-cycle net movement). The implementation reuses existing route paths, requires no database migrations, and extracts pure functions for testability.
## Tasks
- [x] 1. Extract pure backend functions and implement aging endpoint
- [x] 1.1 Create `bucketAgingItems(items)` pure function in `cve-dashboard/backend/routes/compliance.js`
- Export a pure function that accepts an array of `{ seen_count, team }` objects
- Bucket each item into one of four age groups: `1 cycle` (seen_count = 1), `23 cycles` (23), `46 cycles` (46), `7+ cycles` (≥ 7)
- Pivot into one object per bucket with per-team counts and a total, sorted by ascending age
- Return the array in the response shape defined in the design: `[{ bucket, total, STEAM, ACCESS-ENG, ACCESS-OPS, INTELDEV }]`
- _Requirements: 1.1, 1.2, 1.3_
- [x] 1.2 Replace the `/mttr` route handler with the aging endpoint
- Remove the old MTTR SQL query and handler logic
- Add a new handler at `GET /mttr` that queries active compliance items grouped by bucket CASE expression and team
- Use the SQL from the design: `SELECT CASE WHEN seen_count = 1 THEN '1 cycle' ... END AS bucket, team, COUNT(*) AS count FROM compliance_items WHERE status = 'active' GROUP BY bucket, team ORDER BY ...`
- Call `bucketAgingItems()` to pivot the flat rows, then return `{ aging: [...] }`
- Return `{ aging: [] }` with HTTP 200 when no active findings exist
- Return HTTP 500 with `{ error: "Database error" }` on SQLite errors
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 6.3_
- [ ]* 1.3 Write property test for aging bucketing (Property 1)
- **Property 1: Aging bucketing correctness and conservation**
- Create `cve-dashboard/backend/__tests__/compliance-aging-bucketing.property.test.js`
- Generator: array of `{ seen_count: fc.integer({ min: 1, max: 200 }), team: fc.constantFrom('STEAM', 'ACCESS-ENG', 'ACCESS-OPS', 'INTELDEV') }`
- Assert: every item maps to exactly one bucket, per-team counts sum to bucket total, all bucket totals sum to input length
- **Validates: Requirements 1.1, 1.2**
- [ ]* 1.4 Write unit tests for aging endpoint
- Create `cve-dashboard/backend/__tests__/compliance-aging-endpoint.test.js`
- Test correct JSON shape with known seed data (Req 1.3)
- Test empty array returned when no active items exist (Req 1.4)
- Test HTTP 500 returned on DB error (Req 1.5)
- _Requirements: 1.3, 1.4, 1.5_
- [x] 2. Implement waterfall endpoint with pure function
- [x] 2.1 Create `computeWaterfall(uploads)` pure function in `cve-dashboard/backend/routes/compliance.js`
- Export a pure function that accepts an ordered array of `{ report_date, new_count, recurring_count, resolved_count }`
- Iterate with a running accumulator: first row starts at 0, each subsequent row's start = previous row's end
- Compute `end = start + new_count + recurring_count - resolved_count` for each row
- Return array of `{ date, start, new_count, recurring_count, resolved_count, end }`
- _Requirements: 3.1, 3.2, 3.3_
- [x] 2.2 Replace the `/top-recurring` route handler with the waterfall endpoint
- Remove the old persistent-findings SQL query and handler logic
- Add a new handler at `GET /top-recurring` that queries `compliance_uploads` ordered by `report_date ASC`
- Use `COALESCE` for null safety on count columns
- Call `computeWaterfall()` to build the chain, then return `{ waterfall: [...] }`
- Return `{ waterfall: [] }` with HTTP 200 when no uploads exist
- Return HTTP 500 with `{ error: "Database error" }` on SQLite errors
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 6.4_
- [ ]* 2.3 Write property test for waterfall computation (Property 2)
- **Property 2: Waterfall chain linkage and arithmetic invariant**
- Create `cve-dashboard/backend/__tests__/compliance-waterfall-chain.property.test.js`
- Generator: array of `{ new_count: fc.nat({ max: 50 }), recurring_count: fc.nat({ max: 50 }), resolved_count: fc.nat({ max: 50 }), report_date: fc.date().map(d => d.toISOString().slice(0, 10)) }`
- Assert: first row start = 0, each row N start = row N-1 end, each row end = start + new_count + recurring_count - resolved_count
- **Validates: Requirements 3.1, 3.2, 3.3**
- [ ]* 2.4 Write unit tests for waterfall endpoint
- Create `cve-dashboard/backend/__tests__/compliance-waterfall-endpoint.test.js`
- Test empty array returned when no uploads exist (Req 3.5)
- Test HTTP 500 returned on DB error (Req 3.6)
- Test correct chain computation with known seed data (Req 3.1, 3.2, 3.3)
- _Requirements: 3.1, 3.2, 3.3, 3.5, 3.6_
- [x] 3. Checkpoint — Backend complete
- Ensure all tests pass, ask the user if questions arise.
- [x] 4. Implement frontend AgingChart component
- [x] 4.1 Replace `MttrChart` with `AgingChart` in `cve-dashboard/frontend/src/components/pages/ComplianceChartsPanel.js`
- Remove the `MttrChart` function component entirely
- Add `AgingChart({ data })` that renders a vertical stacked `BarChart` using Recharts
- X-axis: bucket labels (`1 cycle`, `23 cycles`, `46 cycles`, `7+ cycles`)
- Y-axis: finding count with monospace axis styling
- One `<Bar>` per team key from `TEAM_COLORS` using `stackId="aging"`
- Use `DarkTooltip` for hover showing bucket label, per-team counts, and total
- Render `<NoData />` when data array is empty
- Wrap in `<ChartCard title="Aging Findings Distribution" subtitle="Active findings by age bucket — stacked by team" />`
- Place in the Chart 4 grid slot previously held by MTTR
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7_
- [ ]* 4.2 Write unit tests for AgingChart
- Test `<NoData />` renders for empty data (Req 2.5)
- Test ChartCard renders with correct title (Req 2.3)
- _Requirements: 2.3, 2.5_
- [x] 5. Implement frontend WaterfallChart component
- [x] 5.1 Replace `RecurringChart` with `WaterfallChart` in `cve-dashboard/frontend/src/components/pages/ComplianceChartsPanel.js`
- Remove the `RecurringChart` function component entirely
- Add `WaterfallChart({ data })` that renders a vertical `BarChart` using Recharts
- Stacked bars: transparent base `<Bar dataKey="start" stackId="w" fill="transparent" />`, red new `<Bar dataKey="new_count" fill="#EF4444" />`, amber recurring `<Bar dataKey="recurring_count" fill="#F59E0B" />`
- Separate bar for resolved: `<Bar dataKey="resolved_count" fill="#10B981" />` (not stacked, matching existing DeltaChart pattern)
- X-axis: cycle dates formatted via `fmtDate`, Y-axis: finding count with monospace styling
- Custom `WaterfallTooltip` showing date, start, new, recurring, resolved, and end values
- Render `<NoData />` when data array is empty
- Wrap in `<ChartCard title="Net Change Waterfall" subtitle="Per-cycle net movement: start → +new → +recurring → resolved → end" />`
- Place in the Chart 5 grid slot previously held by Most Persistent Findings
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8_
- [ ]* 5.2 Write unit tests for WaterfallChart
- Test `<NoData />` renders for empty data (Req 4.5)
- Test ChartCard renders with correct title (Req 4.6)
- _Requirements: 4.5, 4.6_
- [x] 6. Update state and fetch logic in ComplianceChartsPanel
- [x] 6.1 Replace state variables and fetch calls
- Replace `const [mttr, setMttr] = useState([])` with `const [aging, setAging] = useState([])`
- Replace `const [recurring, setRecurring] = useState([])` with `const [waterfall, setWaterfall] = useState([])`
- In the `Promise.all` fetch block, update the `/compliance/mttr` response handler: `setAging(d.aging || [])`
- Update the `/compliance/top-recurring` response handler: `setWaterfall(d.waterfall || [])`
- Keep `/compliance/trends` and `/archer-tickets/status-trend` fetches unchanged
- Pass `aging` to `<AgingChart data={aging} />` and `waterfall` to `<WaterfallChart data={waterfall} />`
- Ensure individual fetch failures render `<NoData />` without affecting other charts
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 6.1, 6.2_
- [ ]* 6.2 Write integration tests for dashboard fetch logic
- Test that dashboard fetches `/compliance/mttr` and `/compliance/top-recurring` on mount (Req 5.1, 5.2)
- Test that `/compliance/trends` and `/archer-tickets/status-trend` are still fetched (Req 5.3)
- Test that all four fetches run in parallel via `Promise.all` (Req 5.4)
- Test that a single fetch failure does not break other charts (Req 5.5)
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5_
- [x] 7. Final checkpoint — All tests pass
- Ensure all tests pass, ask the user if questions arise.
## Notes
- Tasks marked with `*` are optional and can be skipped for faster MVP
- Each task references specific requirements for traceability
- Checkpoints ensure incremental validation after backend and full integration
- Property tests validate universal correctness properties from the design (Properties 1 and 2)
- Unit tests validate specific examples and edge cases
- Pure functions `bucketAgingItems()` and `computeWaterfall()` are extracted first to enable property-based testing
- No new route paths are introduced — `/mttr` and `/top-recurring` are reused in-place
- No database migrations are needed — all data comes from existing columns