Files
cve-dashboard/.kiro/specs/compliance-duplicate-failing-metrics/tasks.md

205 lines
26 KiB
Markdown
Raw Normal View History

# Implementation Plan
## Overview
This implementation plan delivers six SQL-level fixes in `backend/routes/compliance.js` for the cross-vertical duplicate `(hostname, metric_id)` bug class documented in `bugfix.md` and `design.md`. Each fix targets a single endpoint or persistence block:
1. `GET /items``DISTINCT ON (hostname, metric_id)` plus a defensive `groupByHostname` Set
2. `GET /items/:hostname``DISTINCT ON (metric_id, status)` with JS-side sort
3. `GET /vcl/stats` heavy-hitters and per-team totals — `device_team` CTE
4. `GET /vcl/stats` forecast-burndown — `DISTINCT ON (hostname, metric_id)`
5. `GET /mttr``DISTINCT ON (hostname, metric_id)`
6. `persistUpload()` snapshot block — `hostname_status` CTE classifying each hostname via `MIN(status)`
The plan follows the bugfix workflow: a single property-based exploration test (Property 1) covering all six affected sites runs on UNFIXED code first to confirm the bug exists, then preservation property tests (Property 2) capture single-vertical and unique-key behaviour as a baseline. Implementation tasks apply each fix in order (3.13.6), followed by re-running the exploration test (3.7) and the preservation suite (3.8). The checkpoint runs the full backend test suite to confirm no regressions.
## Task Dependency Graph
Tasks are organised into execution waves. Tasks within the same wave may run in parallel; tasks in later waves depend on the completion of earlier waves. Sub-tasks 3.13.6 are independent of each other (each touches a distinct query in `backend/routes/compliance.js`) and form a single parallel wave; sub-tasks 3.7 and 3.8 depend on all six fixes being landed.
```json
{
"waves": [
{
"wave": 1,
"description": "Write the bug-condition exploration property test on UNFIXED code (Property 1, six slices covering /items, /items/:hostname, /vcl/stats heavy-hitters, /vcl/stats forecast-burndown, /mttr, persistUpload snapshot).",
"tasks": ["1"]
},
{
"wave": 2,
"description": "Write the preservation property tests on UNFIXED code (observation-first methodology), recording baseline responses for single-vertical and unique-key fixtures across all five read endpoints and persistUpload(). Property 8.A is written but skipped pending the fix.",
"tasks": ["2"]
},
{
"wave": 3,
"description": "Apply the six SQL-level fixes in backend/routes/compliance.js. Each sub-task targets a distinct query and is independent of the others — sub-tasks 3.1 through 3.6 can be implemented in parallel.",
"tasks": ["3.1", "3.2", "3.3", "3.4", "3.5", "3.6"]
},
{
"wave": 4,
"description": "Re-run the exploration test (3.7) and preservation tests (3.8) against the fixed code. 3.7 confirms all six bug-condition slices now pass; 3.8 confirms preservation properties still pass and unskips Property 8.A to verify the representative-row policy.",
"tasks": ["3.7", "3.8"]
},
{
"wave": 5,
"description": "Checkpoint — run the full backend Jest suite to confirm no regressions in adjacent compliance tests (vcl-compliance-reporting, vcl-aggregated-burndown).",
"tasks": ["4"]
}
]
}
```
## Tasks
- [x] 1. Write bug condition exploration property test
- **Property 1: Bug Condition** - Cross-Vertical Duplicate `(hostname, metric_id)` Distorts Compliance Endpoints
- **CRITICAL**: This test MUST FAIL on unfixed code — failure confirms the bug exists in all six affected sites
- **DO NOT attempt to fix the test or the code when it fails**
- **NOTE**: This test encodes the expected behaviour (Property 16 from design.md). It will validate the fix when it passes after implementation.
- **GOAL**: Surface counterexamples that demonstrate cross-vertical duplicate `(hostname, metric_id)` rows distort `/items`, `/items/:hostname`, `/vcl/stats` heavy-hitters, `/vcl/stats` forecast-burndown, `/mttr`, and the `persistUpload()` snapshot block
- **Scoped PBT Approach**: Six concrete failing scenarios (one per affected site) generated by fast-check from a small fixed input space — each scenario seeds two `compliance_items` rows for the same `(hostname, metric_id)` across `vertical IS NULL` and `vertical = 'NTS_AEO'`. Use `fast-check` (already in use under `backend/__tests__/*.property.test.js`).
- Place the test at `backend/__tests__/compliance-duplicate-failing-metrics.exploration.property.test.js`. Mock `../db` with `jest.mock` exactly like `vcl-compliance-reporting.property.test.js` so route handlers can be invoked against an in-memory fixture, or stand up a transactional `pg` test schema if the repo already supports one — match whichever convention the existing compliance tests use.
- **Slice 1.A — `/items` failing-metrics dedup (Bug Condition isBugCondition: two active rows for `(STEAM-INTERSIGHT, 7.1.1)`, one `vertical IS NULL`, one `vertical = 'NTS_AEO'`, both `team = 'STEAM'`)**
- Seed `fixtureCrossVerticalDuplicateActive` from design.md §Test Fixtures (different `seen_count`: 3 and 5)
- Call `GET /items?team=STEAM&status=active`
- Assert `response.devices[0].failing_metrics.filter(m => m.metric_id === '7.1.1').length === 1` (Property 1 from design.md)
- **EXPECTED OUTCOME on unfixed code**: FAILS — `failing_metrics` contains two `7.1.1` entries because `groupByHostname` pushes per row
- **Slice 1.B — `/items/:hostname` `(metric_id, status)` dedup (same fixture)**
- Call `GET /items/STEAM-INTERSIGHT`
- Assert `response.metrics.filter(m => m.metric_id === '7.1.1' && m.status === 'active').length === 1` (Property 2 from design.md)
- Assert the surviving entry's `seen_count === 5` (max across duplicates per Property 8)
- **EXPECTED OUTCOME on unfixed code**: FAILS — detail query has no vertical filter and no dedup, returns two `7.1.1/active` entries
- **Slice 1.C — `/vcl/stats` heavy-hitters cross-team (Bug Condition: two active rows for the same `(hostname, metric_id)` whose `team` differs across verticals)**
- Seed `fixtureCrossVerticalTeamMismatch` from design.md §Test Fixtures (`team = 'STEAM'` legacy, `team = 'ACCESS-ENG'` NTS_AEO)
- Call `GET /vcl/stats`
- Assert `SUM(heavy_hitters[*].non_compliant) === stats.non_compliant` (Property 3 from design.md)
- **EXPECTED OUTCOME on unfixed code**: FAILS — sum exceeds the global count by 1 because `COUNT(DISTINCT hostname) GROUP BY team` counts the hostname under both teams
- **Slice 1.D — `/vcl/stats` forecast-burndown blockers (Bug Condition: two active rows for the same `(hostname, metric_id)` both with non-null `resolution_date`, same team)**
- Seed `fixtureForecastDuplicateResolutionDate` from design.md §Test Fixtures (`team = 'STEAM'`, both `resolution_date = '2025-09-30'`)
- Call `GET /vcl/stats`, locate the `STEAM` entry in `vertical_breakdown`
- Assert the unclamped `teamNonCompliant - forecastItems.length === blockers` AND `blockers >= 0` (Property 4 from design.md)
- **EXPECTED OUTCOME on unfixed code**: FAILS — unclamped value is `-1`, route reports `0` after `Math.max(blockers, 0)`, hiding the inconsistency. The unclamped check makes the failure visible
- **Slice 1.E — `/mttr` aging buckets (same fixture as Slice 1.A, with `seen_count = 5` on both rows)**
- Call `GET /mttr`
- Assert `SUM(aging[*].total) === COUNT(DISTINCT (hostname, metric_id) WHERE status = 'active')` over the seeded items (Property 5 from design.md)
- **EXPECTED OUTCOME on unfixed code**: FAILS — `bucketAgingItems()` increments the `46 cycles` bucket twice, exceeding the distinct count by 1
- **Slice 1.F — `persistUpload()` snapshot block (Bug Condition: same `(hostname, metric_id, team)` with `status = 'active'` in legacy and `status = 'resolved'` in NTS_AEO)**
- Seed `fixtureCrossVerticalStatusMismatch` from design.md §Test Fixtures
- Run `persistUpload()` with a no-op upload, read back the `compliance_snapshots` row for the current month and `STEAM`
- Assert `compliant + non_compliant <= total_devices` (Property 6 from design.md)
- **EXPECTED OUTCOME on unfixed code**: FAILS — the hostname is counted in both `compliant` and `non_compliant`, sum exceeds `total_devices` by 1
- Run on UNFIXED code and capture all six counterexamples in the test output
- Document the six counterexamples in the test file (a leading comment block listing the failing slice → symptom mapping) so the bug surface is recoverable from the test alone
- Mark task complete when the test is written, run, and all six slice failures are documented
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7_
- [x] 2. Write preservation property tests (BEFORE implementing fix)
- **Property 2: Preservation** - Single-Vertical and Unique-Key Inputs Are Byte-For-Byte Unchanged
- **IMPORTANT**: Follow observation-first methodology — observe behaviour on UNFIXED code, then write property tests that capture it
- Place the test at `backend/__tests__/compliance-duplicate-failing-metrics.preservation.property.test.js`. Use `fast-check` and the same `jest.mock('../db', ...)` pattern as the existing property tests
- Build the unique-key generator: `fc.array(...)` of `compliance_items` rows where every `(hostname, metric_id)` pair is unique across the array. Mix `vertical IS NULL`, `vertical = 'NTS_AEO'`, and other verticals; mix `status = 'active'` and `status = 'resolved'`; mix teams from `ALLOWED_TEAMS`
- **Observation step**: For each fixture from design.md §Test Fixtures (`fixtureLegacyOnly`, `fixtureNTSAEOOnly`, `fixtureMultiHostnameMixed` restricted to its unique-key subset, plus a hand-built empty-state fixture), run all five read endpoints + `persistUpload()` on UNFIXED code and capture responses. Persist these as snapshot fixtures alongside the test (e.g., a small JSON file under `backend/__tests__/fixtures/compliance-duplicate-failing-metrics/`) so the test compares against the recorded baseline rather than against a moving target
- **Property 7.A — `/items` unique-key preservation**: For any unique-key fixture, assert the response from `GET /items?team=...&status=...` (across `team ∈ ALLOWED_TEAMS` and `status ∈ {'active', 'resolved'}`) equals the recorded baseline byte-for-byte (deep equality on the JSON response)
- **Property 7.B — `/items/:hostname` unique-key preservation**: For any unique-key fixture, assert the response from `GET /items/:hostname` for every seeded hostname equals the recorded baseline. Also assert active-then-resolved ordering is preserved (`response.metrics[0..k].status === 'active'` then `response.metrics[k+1..].status === 'resolved'`, sorted by `metric_id` within each group), per design.md §Preservation Requirements item 5
- **Property 7.C — `/vcl/stats` unique-key preservation**: Assert response equality across `stats.compliant`, `stats.non_compliant`, the `donut` block (`blocked` / `in_progress`), `heavy_hitters` (full array), and `vertical_breakdown` (full array including `blockers`). Generate inputs that vary `resolution_date` density to exercise the donut categorisation
- **Property 7.D — `/mttr` unique-key preservation**: Assert response equality on the full `aging` array (per-bucket per-team totals)
- **Property 7.E — `persistUpload()` unique-key preservation**: For a single-status-per-hostname fixture (every hostname has only `active` or only `resolved` rows, never both, within a team), run `persistUpload()` and assert the `compliance_snapshots` rows for the current `(snapshot_month, vertical)` are identical to the recorded baseline
- **Property 7.F — `/items` query-param validation preservation**: Assert that `?team=OTHER` (not in `ALLOWED_TEAMS`) returns HTTP 400 and `?status=invalid` returns HTTP 400, on both unfixed and fixed code. Assert `/items/:hostname` for an unknown hostname returns HTTP 404
- **Property 8.A — Representative-row policy on duplicates**: For inputs WITH duplicates, assert the surviving entry carries `seen_count = MAX(seen_count)`, `first_seen = MIN(first_seen)`, `last_seen = MAX(last_seen)` across the duplicate rows (this is the only preservation property that exercises the duplicate path, since it specifies WHAT the dedup must produce — keep it in the preservation file because it defines the contract the fix must satisfy)
- Run the full preservation suite on UNFIXED code
- **EXPECTED OUTCOME**: Properties 7.A7.F PASS on unfixed code (they describe baseline behaviour to preserve). Property 8.A is the only one expected to FAIL on unfixed code, since it asserts the post-fix representative-row contract — exclude it from the unfixed-code run or mark it as `test.skip` until the fix lands, with a comment pointing to task 3.5
- Mark task complete when 7.A7.F are written, run, and passing on unfixed code, and 8.A is written but skipped pending the fix
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7_
- [x] 3. Fix for cross-vertical duplicate `(hostname, metric_id)` rows distorting compliance endpoints
- [x] 3.1 Implement Fix 1: `GET /items` `DISTINCT ON (hostname, metric_id)` and defensive `groupByHostname` dedup
- Edit `backend/routes/compliance.js`, `router.get('/items', ...)` (around line 535)
- Rewrite the items query as `SELECT DISTINCT ON (ci.hostname, ci.metric_id) ... ORDER BY ci.hostname, ci.metric_id, ci.seen_count DESC, ci.upload_id DESC` keeping the existing `WHERE ci.team = $1 AND ci.status = $2 AND (ci.vertical IS NULL OR ci.vertical = 'NTS_AEO')` predicate intact (per design.md Fix 1 step 1 and Preservation Requirements bullet 4)
- Edit `groupByHostname()` (around line 213) to add a `_seenMetricIds` Set per device, push `failing_metrics` only when the `metric_id` has not been seen, aggregate `seen_count` via `Math.max`, `first_seen` via `Math.min`, `last_seen` via `Math.max`, and strip `_seenMetricIds` from the returned device object so the response shape is unchanged (per design.md Fix 1 step 2)
- _Bug_Condition: isBugCondition(items) — two or more rows share `(hostname, metric_id)` across verticals (design.md §Bug Condition)_
- _Expected_Behavior: Property 1 — `device.failing_metrics.length === new Set(device.failing_metrics.map(m => m.metric_id)).size` (design.md §Correctness Properties Property 1)_
- _Preservation: Single-vertical and unique-key inputs unchanged; `(ci.vertical IS NULL OR ci.vertical = 'NTS_AEO')` predicate retained; `team`/`status` query-param validation unchanged (design.md §Preservation Requirements bullets 1, 4, 7)_
- _Requirements: 2.1, 3.1, 3.3, 3.4_
- [x] 3.2 Implement Fix 2: `GET /items/:hostname` `DISTINCT ON (metric_id, status)` and JS sort
- Edit `backend/routes/compliance.js`, `router.get('/items/:hostname', ...)` (around line 575)
- Rewrite the metrics query as `SELECT DISTINCT ON (ci.metric_id, ci.status) ... FROM compliance_items ci ... WHERE ci.hostname = $1 ORDER BY ci.metric_id, ci.status, ci.seen_count DESC, ci.upload_id DESC` (per design.md Fix 2 step 1)
- Add a JS-side sort on the deduped rows that reproduces the original `ORDER BY ci.status DESC, ci.metric_id` ordering — `metricRows.sort((a, b) => a.status !== b.status ? b.status.localeCompare(a.status) : a.metric_id.localeCompare(b.metric_id))` (per design.md Fix 2 step 2)
- Leave the existing `metricRows.find(r => r.status === 'active') || metricRows[0]` identity lookup unchanged (per design.md Fix 2 step 3)
- _Bug_Condition: isBugCondition(items) plus duplicate rows for the same `hostname` across verticals with no vertical filter on the detail query_
- _Expected_Behavior: Property 2 — exactly one entry per `(metric_id, status)` pair, surviving entry carries `MAX(seen_count)` across duplicates_
- _Preservation: active-before-resolved ordering, `metric_id` ordering within each status group, identity lookup unchanged_
- _Requirements: 2.2, 2.3, 3.2, 3.3_
- [x] 3.3 Implement Fix 3: `/vcl/stats` heavy-hitters and per-team totals via `device_team` CTE
- Edit `backend/routes/compliance.js`, `router.get('/vcl/stats', ...)` (around line 990, the `teamRows` query)
- Replace the heavy-hitters query with a CTE: `WITH device_team AS (SELECT DISTINCT ON (hostname) hostname, COALESCE(team, 'Unknown') AS team, resolution_date FROM compliance_items WHERE status = 'active' ORDER BY hostname, seen_count DESC, upload_id DESC) SELECT team, COUNT(DISTINCT hostname)::int AS non_compliant, MAX(resolution_date) AS compliance_date FROM device_team GROUP BY team ORDER BY COUNT(DISTINCT hostname) DESC` (per design.md Fix 3 step 1)
- Rewrite the per-team-total query inside the `for (const teamRow of teamRows)` loop to use the same `device_team`-style CTE: `WITH device_team AS (SELECT DISTINCT ON (hostname) hostname, COALESCE(team, 'Unknown') AS team FROM compliance_items ORDER BY hostname, seen_count DESC, upload_id DESC) SELECT COUNT(*)::int AS total FROM device_team WHERE team = $1` (per design.md Fix 3 step 3)
- Leave the global `stats.non_compliant` query unchanged — it is already correct (per design.md Fix 3 step 2)
- _Bug_Condition: a hostname's `team` differs across verticals so `COUNT(DISTINCT hostname) GROUP BY team` counts it under both teams_
- _Expected_Behavior: Property 3 — `SUM(heavy_hitters[*].non_compliant) === stats.non_compliant` and each hostname assigned to exactly one team (the team from its representative row)_
- _Preservation: global `stats.compliant` / `stats.non_compliant` unchanged, donut categorisation unchanged_
- _Requirements: 2.5, 3.6_
- [x] 3.4 Implement Fix 4: `/vcl/stats` forecast-burndown `DISTINCT ON (hostname, metric_id)`
- Edit `backend/routes/compliance.js`, `router.get('/vcl/stats', ...)` (around line 1015, the `forecastItems` query)
- Rewrite as `SELECT DISTINCT ON (hostname, metric_id) resolution_date FROM compliance_items WHERE status = 'active' AND COALESCE(team, 'Unknown') = $1 AND resolution_date IS NOT NULL ORDER BY hostname, metric_id, seen_count DESC, upload_id DESC` (per design.md Fix 4 step 1)
- Leave `blockers = teamNonCompliant - forecastItems.length` and the `Math.max(blockers, 0)` clamp unchanged — the clamp becomes a no-op in correct operation but stays as belt-and-braces (per design.md Fix 4 steps 2 and 3)
- _Bug_Condition: duplicate `(hostname, metric_id)` rows with non-null `resolution_date` inflate `forecastItems.length` past `teamNonCompliant`_
- _Expected_Behavior: Property 4 — unclamped `teamNonCompliant - dedupedForecastCount === blockers` and `blockers >= 0`_
- _Preservation: `Math.max(blockers, 0)` clamp retained as a no-op safeguard, downstream forecast bucketing unchanged_
- _Requirements: 2.6, 3.6_
- [x] 3.5 Implement Fix 5: `/mttr` `DISTINCT ON (hostname, metric_id)` in SQL
- Edit `backend/routes/compliance.js`, `router.get('/mttr', ...)` (around line 824)
- Rewrite the query as `SELECT DISTINCT ON (hostname, metric_id) COALESCE(seen_count, 1) AS seen_count, team FROM compliance_items WHERE status = 'active' ORDER BY hostname, metric_id, seen_count DESC, upload_id DESC` (per design.md Fix 5 step 1)
- Leave `bucketAgingItems()` unchanged — its contract is preserved because it is also called from `/vcl/stats` (per design.md Fix 5 steps 2 and 3)
- _Bug_Condition: duplicate active rows for the same `(hostname, metric_id)` are bucketed twice by `bucketAgingItems()`_
- _Expected_Behavior: Property 5 — `SUM(aging[*].total) === COUNT(DISTINCT (hostname, metric_id) WHERE status = 'active')` and each unique violation bucketed exactly once with a single representative `seen_count`_
- _Preservation: `bucketAgingItems()` contract unchanged, per-team buckets on single-vertical fixtures unchanged_
- _Requirements: 2.7, 3.7_
- [x] 3.6 Implement Fix 6: `persistUpload()` snapshot via `hostname_status` CTE
- Edit `backend/routes/compliance.js`, `persistUpload()` (lines 81192), specifically the `verticalStats` query at line 157
- Rewrite the snapshot query as `WITH hostname_status AS (SELECT team, hostname, MIN(status) AS status FROM compliance_items WHERE team IS NOT NULL GROUP BY team, hostname) SELECT team AS vertical, COUNT(*)::int AS total_devices, COUNT(*) FILTER (WHERE status = 'resolved')::int AS compliant, COUNT(*) FILTER (WHERE status = 'active')::int AS non_compliant FROM hostname_status GROUP BY team` (per design.md Fix 6 step 1)
- Leave the downstream `INSERT ... ON CONFLICT (snapshot_month, vertical) DO UPDATE` block and `compliance_pct` calculation unchanged (per design.md Fix 6 steps 2 and 3)
- _Bug_Condition: a hostname has both `active` and `resolved` rows for the same team across verticals, so the `CASE WHEN status = 'X' THEN hostname END` pattern lets it appear in both `compliant` and `non_compliant`_
- _Expected_Behavior: Property 6 — every snapshot row satisfies `compliant + non_compliant === total_devices` (active wins over resolved via `MIN(status)`)_
- _Preservation: snapshot rows for single-status-per-hostname fixtures unchanged, error-handling try/catch unchanged_
- _Requirements: 2.4, 3.5_
- [x] 3.7 Verify bug condition exploration test now passes
- **Property 1: Expected Behavior** - Cross-Vertical Duplicate `(hostname, metric_id)` Distorts Compliance Endpoints
- **IMPORTANT**: Re-run the SAME test from task 1 — do NOT write a new test
- The test from task 1 encodes the expected behaviour for all six slices (1.A1.F). When all six slices pass, the bug is fixed across every affected site
- Run `npx jest backend/__tests__/compliance-duplicate-failing-metrics.exploration.property.test.js --runInBand` (or the repo's equivalent jest invocation)
- **EXPECTED OUTCOME**: All six slices PASS — Slice 1.A (`/items` dedup), Slice 1.B (`/items/:hostname` dedup), Slice 1.C (`/vcl/stats` heavy-hitters), Slice 1.D (`/vcl/stats` forecast blockers), Slice 1.E (`/mttr` aging), Slice 1.F (`persistUpload()` snapshot invariant)
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7_
- [x] 3.8 Verify preservation tests still pass and unskip Property 8.A
- **Property 2: Preservation** - Single-Vertical and Unique-Key Inputs Are Byte-For-Byte Unchanged
- **IMPORTANT**: Re-run the SAME tests from task 2 — do NOT write new tests
- Unskip Property 8.A (the representative-row policy assertion) which was deferred in task 2 because it asserts the post-fix contract
- Run `npx jest backend/__tests__/compliance-duplicate-failing-metrics.preservation.property.test.js --runInBand`
- **EXPECTED OUTCOME**: Properties 7.A7.F continue to PASS (no regressions on unique-key or single-vertical inputs) and Property 8.A now PASSES on the duplicate path (`seen_count = MAX`, `first_seen = MIN`, `last_seen = MAX` across duplicates)
- Confirm the recorded baseline JSON snapshots match the post-fix output for every unique-key fixture
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7_
- [x] 4. Checkpoint - Run full backend test suite and confirm all tests pass
- Run the full backend test suite from `backend/`: `npx jest --runInBand` (or the repo's standard test command)
- Confirm both new property test files pass and that no existing tests under `backend/__tests__/` regressed — particularly `vcl-compliance-reporting.property.test.js`, `vcl-aggregated-burndown.property.test.js`, and `vcl-compliance-reporting.test.js`, all of which exercise overlapping compliance code paths
- If any pre-existing test fails, diagnose whether the failure is a genuine regression introduced by the fix or a pre-existing flake. If a regression, return to the relevant sub-task in step 3 and adjust the fix; do not silence the failing test
- Ask the user if any unexpected questions arise about test scope, fixture naming, or whether any preservation snapshot needs to be re-recorded against the fixed code
- Mark complete when the full suite is green
## Notes
- **Fix sequencing**: Tasks 3.1 through 3.6 are independent — each one targets a distinct query and can be implemented and committed in any order. Implementers may parallelise work across these sub-tasks, but tasks 3.7 and 3.8 depend on all six fixes being landed before they can validate.
- **Test framework**: Both new property tests follow the existing `backend/__tests__/*.property.test.js` convention — `fast-check` for generators, `jest.mock('../db', ...)` for mocking the `pg` pool, and matching helper imports (`auditLog`, `ivantiApi`) where required. See `vcl-compliance-reporting.property.test.js` for the canonical pattern.
- **Fixture location**: Place fixtures at `backend/__tests__/fixtures/compliance-duplicate-failing-metrics/` if a directory-based layout is preferred, or inline as factory functions in the test files. Match whichever convention the existing compliance test files use — if they inline factories (as `vcl-compliance-reporting.test.js` does), follow suit.
- **Property 8.A skip**: Property 8.A in task 2 is intentionally skipped on unfixed code because it asserts the post-fix representative-row contract. Task 3.8 unskips it. This is the only test that exercises the duplicate path inside the preservation file; it lives there because the contract it captures is precisely the one preservation must not violate after the fix lands.
- **Adjacent spec coordination**: Fix 6 (`persistUpload()` snapshot) is conceptually adjacent to but mechanically distinct from the `persistUpload` fix in `compliance-duplicate-chart-entries`. That spec adds a `WHERE vertical = $1` filter to scope the snapshot to one vertical; this spec adds the `hostname_status` CTE so each hostname is classified once within whichever vertical is snapshotted. Both are independently necessary. If both specs land in the same release, ensure the merged query carries both the vertical filter AND the `hostname_status` CTE.
- **`Math.max(blockers, 0)` clamp**: Left in place as a belt-and-braces safeguard per design.md Fix 4 step 3. After the fix it becomes a no-op; if a future regression reintroduces inconsistency, Property 4 (Slice 1.D) catches it before the clamp can mask the underlying bug.
- **Documentation follow-up**: Per `.kiro/steering/workflow.md`, after the fix lands and is committed to `master`, add a bug report under `docs/bug-reports/` on the `ops/records` branch using the `compliance-duplicate-failing-metrics-<YYYY-MM-DD>.md` naming convention. Each of the six fix sites is a separate `## Bug N` entry following the Symptom → Cause → Fix triad.