22 KiB
Implementation Plan
Overview
Five compliance code paths share the root cause "key by compliance_uploads.id instead of by compliance_uploads.report_date": GET /trends, GET /top-recurring, GET /category-trend, GET /summary, and the compliance_snapshots block inside persistUpload(). All fixes are contained to backend/routes/compliance.js and require no schema migration, no new column, and no frontend change.
The plan follows the bugfix workflow's exploratory methodology: a single property-based test file (backend/__tests__/compliance-duplicate-chart-entries.property.test.js) is written before any fix, with one test case per affected site demonstrating the bug condition, plus preservation cases observed on the unfixed code. Each fix is then implemented as its own task and verified by re-running the matching test case from the exploration suite. The plan ends with a regression checkpoint that re-runs the full preservation suite and the backend test suite.
Task Dependency Graph
{
"waves": [
{ "id": 0, "tasks": ["1", "2"] },
{ "id": 1, "tasks": ["3.1", "4.1", "5.1", "6.1", "7.1"] },
{ "id": 2, "tasks": ["3.2", "4.2", "5.2", "6.2", "7.2"] },
{ "id": 3, "tasks": ["8"] },
{ "id": 4, "tasks": ["9"] }
]
}
Wave 0 establishes the test baseline: task 1 documents Property 1 (bug condition) failures on the unfixed code and task 2 captures Property 2 (preservation) baseline outputs. Wave 1 implements the five independent fixes in backend/routes/compliance.js (no inter-fix dependencies — each touches a different SQL statement). Wave 2 verifies each fix's slice of Property 1 now passes. Wave 3 re-runs the full Property 2 suite to confirm no regressions across the five sites. Wave 4 is the final checkpoint that runs the entire backend test suite.
Tasks
-
1. Write bug condition exploration property test
- Property 1: Bug Condition - Multi-Vertical Date Aggregation Across Five Compliance Sites
- CRITICAL: This test MUST FAIL on unfixed code — failure confirms the bug exists across all five sites
- DO NOT attempt to fix the test or the code when it fails
- NOTE: This test encodes the expected behavior — it will validate the fixes when it passes after implementation
- GOAL: Surface counterexamples that demonstrate the bug exists for each of the five affected code paths (
/trends,/top-recurring,/category-trend,/summary,persistUpload()snapshot block) - Scoped PBT Approach: Scope the property to the canonical bug-condition fixture (
fixture_multi_vertical_single_datefrom design.md) — three uploads for2025-05-11, one each forNTS_AEO,SDIT_CISO,TSI, with distinct counts and matching items per upload — this reproduces the original GitLab #12 scenario deterministically - Bug Condition (from design.md):
EXISTS report_date d WHERE COUNT(compliance_uploads WHERE report_date = d) > 1 - Create
backend/__tests__/compliance-duplicate-chart-entries.property.test.jsusingfast-checkand the existing pg pool mock pattern fromvcl-compliance-reporting.property.test.js - Test case 1.A —
/trendsduplicate-date counterexample: seed three uploads for2025-05-11(verticals NTS_AEO/SDIT_CISO/TSI), callGET /trends, assertresponse.trends.filter(t => t.report_date === '2025-05-11').length === 1ANDnew_countfor that date equals the sum of the three uploads'new_countvalues (likewiserecurring_count,resolved_count,total_active, and per-team counts) - Test case 1.B —
/top-recurringduplicate-bar counterexample: same fixture, callGET /top-recurring, assert exactly one waterfall entry per uniquereport_dateAND the running invariantentry[i].end === entry[i].start + entry[i].new_count + entry[i].recurring_count - entry[i].resolved_countholds for everyiANDentry[i].start === entry[i-1].endfor adjacent entries (withentry[0].start === 0) - Test case 1.C —
/category-trendduplicate (date, category) counterexample: same fixture plus items tagged with two categories (PatchingandConfiguration), callGET /category-trend, assertresponse.categoryTrend.filter(c => c.report_date === '2025-05-11' && c.category === 'Patching').length === 1AND each entry'scountequals the totalcompliance_itemsfor that category across every upload sharing the date - Test case 1.D —
/summarysibling-disclosure counterexample: same fixture (2025-05-11is the latest date), callGET /summary, assert either (a)entriesis the merged view of all three uploads OR (b)response.multi_vertical_uploadsis a non-empty array withlength === 2listing the other two uploads' ids and verticals - Test case 1.E —
persistUpload()cross-vertical contamination counterexample: pre-populatecompliance_itemswith disjoint sets for two verticals (e.g., NTS_AEO has 100 active items, SDIT_CISO has 50 active items), callpersistUpload()for a fresh SDIT_CISO upload, read back thecompliance_snapshotsrow for the current month withvertical = 'SDIT_CISO', asserttotal_devicesreflects only SDIT_CISO items and is not inflated by NTS_AEO items - Wrap each test case in fast-check
fc.assertagainstarbScenariofrom design.md (uploads with possibly collidingreport_date, items referencing those uploads) so PBT also exercises larger random fixtures beyond the canonical 3-upload case - Add fixture builders in the test file matching the design's
fixture_multi_vertical_single_dateandfixture_cross_vertical_items - Run test on UNFIXED code:
npm run test:backend -- compliance-duplicate-chart-entries.property.test.js - EXPECTED OUTCOME: All five test cases FAIL (this is correct — it proves each manifestation of the bug exists)
- Document the counterexamples found (e.g.,
/trendsreturns 3 entries for2025-05-11instead of 1,/summaryreturns one upload'ssummary_jsonand silently drops the other two,compliance_snapshots.total_devicesfor SDIT_CISO equals 150 instead of 50) - Mark task complete when test is written, run, and the failures for all five test cases are documented
- Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 1.10, 1.11
-
2. Write preservation property tests (BEFORE implementing fix)
- Property 2: Preservation - Single-Upload-Per-Date Behavior Unchanged Across Five Sites
- IMPORTANT: Follow observation-first methodology — capture pre-fix outputs and assert equality post-fix
- Bug Condition negation (from design.md):
FORALL report_date d, COUNT(compliance_uploads WHERE report_date = d) <= 1 - Add preservation test cases to
backend/__tests__/compliance-duplicate-chart-entries.property.test.js - Observe baseline behavior on UNFIXED code using the design's preservation fixtures and capture exact response bodies (snapshot-test style)
- Test case 2.A — Empty-state preservation (
fixture_empty): nocompliance_uploads, nocompliance_items. ObserveGET /trendsreturns{ trends: [] },GET /top-recurringreturns{ waterfall: [] },GET /category-trendreturns{ categoryTrend: [] },GET /summaryreturns{ entries: [], overall_scores: {}, upload: null }. Capture and assert these exact shapes - Test case 2.B — Single AEO-legacy-upload preservation (
fixture_single_upload_aeo_legacy): one upload withvertical IS NULL,report_date = '2025-04-01', ~20 items across the four teams. Observe responses from all four read endpoints, capture them, and assert byte-for-byte equality - Test case 2.C — Multiple single-upload-per-date preservation (
fixture_single_upload_per_date): five uploads on five distinct dates with variedverticalvalues. Observe responses from all four read endpoints and assert equality - Test case 2.D —
/summaryteamquery parameter preservation: with the latest upload present, assert?team=STEAMfiltersentriesserver-side AND?team=OTHER(non-ALLOWED_TEAMS) returns HTTP 400. Capture both responses - Test case 2.E —
persistUpload()single-vertical-month preservation (fixture_cross_vertical_itemsreduced to one vertical): pre-populatecompliance_itemswith rows from a single vertical only, runpersistUpload()for that vertical, capture the resultingcompliance_snapshotsrows - Test case 2.F —
persistUpload()snapshot error-path preservation: force a snapshot query failure (mockpool.queryto reject on the snapshot statement only), assert the upload still commits and the error is logged but not surfaced (HTTP 200/201, no error response) - Property-based extension — Cross-endpoint preservation: use fast-check
arbScenarioconstrained to scenarios where everyreport_datehas exactly one upload row. Assert that for every generated scenario, all four endpoint responses on UNFIXED code match the captured-baseline shape and field-level equality holds (this generator covers the design'sfixture_pbt_generators.arbScenariorestricted to the non-bug-condition input space) - Run tests on UNFIXED code:
npm run test:backend -- compliance-duplicate-chart-entries.property.test.js - EXPECTED OUTCOME: All preservation test cases PASS (this confirms the baseline behavior to preserve)
- Mark task complete when tests are written, run, and passing on unfixed code
- Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 3.10
-
3. Fix
/trends— aggregate uploads and team counts byreport_date-
3.1 Rewrite the
/trendsupload and team queries to group byreport_date- In
backend/routes/compliance.jsrouter.get('/trends', ...)(around line 768), replace thecompliance_uploadsquery with theGROUP BY report_dateSQL from design.md Fix 1, summingnew_count,recurring_count,resolved_count, and(new_count + recurring_count) AS total_active - Replace the per-team
compliance_itemsquery with theJOIN compliance_uploads+GROUP BY cu.report_date, ci.teamform from design.md Fix 1 - Change the
teamMapkeyed lookup fromteamMap[u.id]toteamMap[u.report_date]and rebuildtrendsfrom the per-date upload rows - Bug_Condition: isBugCondition(uploads) where two or more compliance_uploads rows share a
report_date - Expected_Behavior: GET /trends returns one entry per unique report_date with summed count fields and aggregated per-team counts (Property 1 from design)
- Preservation: Single-upload-per-date dates produce identical responses; empty-data response remains
{ trends: [] }; chart components require no changes - Requirements: 2.1, 2.2, 2.3, 3.1, 3.2, 3.3
- In
-
3.2 Verify the
/trendsportion of bug condition exploration test now passes- Property 1: Expected Behavior -
/trendsReturns One Entry Per Unique report_date - IMPORTANT: Re-run the SAME test case 1.A from task 1 — do NOT write a new test
- Run
npm run test:backend -- compliance-duplicate-chart-entries.property.test.js -t "/trends" - EXPECTED OUTCOME: Test case 1.A PASSES (confirms
/trendsbug is fixed) - Requirements: Property 1 (Validates 2.1, 2.2, 2.3) from design
- Property 1: Expected Behavior -
-
-
4. Fix
/top-recurring— aggregate uploads byreport_datebefore passing tocomputeWaterfall()-
4.1 Rewrite the
/top-recurringupload query to group byreport_date- In
backend/routes/compliance.jsrouter.get('/top-recurring', ...)(around line 818), replace the query with theGROUP BY report_dateSQL from design.md Fix 2, summingnew_count,recurring_count,resolved_count - Leave
computeWaterfall()unchanged — it already advancesstartcorrectly when fed one row per date; the fix is purely in the SQL - Bug_Condition: isBugCondition(uploads) where two or more compliance_uploads rows share a
report_date - Expected_Behavior: GET /top-recurring returns one waterfall entry per unique report_date with summed deltas; running invariant
entry[i].end === entry[i].start + entry[i].new_count + entry[i].recurring_count - entry[i].resolved_countholds andentry[i].start === entry[i-1].endfor adjacent entries (Property 2 from design) - Preservation: Single-upload-per-date waterfall is unchanged; empty-data response remains
{ waterfall: [] } - Requirements: 2.4, 2.5, 3.4
- In
-
4.2 Verify the
/top-recurringportion of bug condition exploration test now passes- Property 1: Expected Behavior -
/top-recurringHas One Bar Per Unique report_date With Correct Running Totals - IMPORTANT: Re-run the SAME test case 1.B from task 1 — do NOT write a new test
- Run
npm run test:backend -- compliance-duplicate-chart-entries.property.test.js -t "/top-recurring" - EXPECTED OUTCOME: Test case 1.B PASSES (confirms
/top-recurringbug is fixed and the running invariant holds) - Requirements: Property 2 (Validates 2.4, 2.5) from design
- Property 1: Expected Behavior -
-
-
5. Fix
/category-trend— dropcu.idfromGROUP BY-
5.1 Rewrite the
/category-trendquery to group by(report_date, category)only- In
backend/routes/compliance.jsrouter.get('/category-trend', ...)(around line 838), replace the query with the SQL from design.md Fix 3 — removecu.idfrom theGROUP BYso grouping is by(cu.report_date, COALESCE(ci.category, 'Unknown'))only - Leave the response shape
{ categoryTrend: Array<{ report_date, category, count }> }unchanged - Bug_Condition: isBugCondition(uploads) where two or more compliance_uploads rows share a
report_date - Expected_Behavior: GET /category-trend returns one row per unique (report_date, category) pair with
countequal to the totalcompliance_itemsfor that category across every upload sharing the date (Property 3 from design) - Preservation: Single-upload-per-date rows are unchanged; empty-data response remains
{ categoryTrend: [] }; total-conservation property holds across all dates (Property 6 from design) - Requirements: 2.6, 2.7, 3.5
- In
-
5.2 Verify the
/category-trendportion of bug condition exploration test now passes- Property 1: Expected Behavior -
/category-trendReturns One Row Per (date, category) - IMPORTANT: Re-run the SAME test case 1.C from task 1 — do NOT write a new test
- Run
npm run test:backend -- compliance-duplicate-chart-entries.property.test.js -t "/category-trend" - EXPECTED OUTCOME: Test case 1.C PASSES (confirms
/category-trendbug is fixed) - Requirements: Property 3 (Validates 2.6, 2.7) from design
- Property 1: Expected Behavior -
-
-
6. Fix
/summary— disclose sibling uploads for the latest date-
6.1 Add sibling-upload disclosure to the
/summaryresponse- In
backend/routes/compliance.jsrouter.get('/summary', ...)(around line 495), keep the existingvertical IS NULL→vertical = 'NTS_AEO'fallback for selecting the primary upload'ssummary_json(preserves legacy single-upload behavior per requirement 3.6) - After resolving
latestUpload, run the second query from design.md Fix 4 to find sibling uploads sharing the samereport_date:SELECT id, vertical, uploaded_at FROM compliance_uploads WHERE report_date = $1 AND id != $2 ORDER BY id ASC - Add
multi_vertical_uploadsto the response, populated withsiblings.map(s => ({ id, vertical, uploaded_at })); the field is[]when no siblings exist - Do not change the
teamquery parameter handling, theALLOWED_TEAMSHTTP 400 response, or theentries/overall_scores/uploadshape - Bug_Condition: isBugCondition(uploads) where two or more compliance_uploads rows share the latest
report_date - Expected_Behavior: GET /summary either merges sibling uploads' entries OR exposes a non-empty
multi_vertical_uploadsarray identifying the other uploads for the samereport_date; sibling uploads are never silently dropped (Property 4 from design) - Preservation: Single-upload-per-date
/summaryshape is unchanged (thevertical IS NULL→vertical = 'NTS_AEO'fallback still runs);teamquery parameter still filters entries and rejects non-ALLOWED_TEAMSwith HTTP 400; empty-data response remains{ entries: [], overall_scores: {}, upload: null } - Requirements: 2.8, 2.9, 3.6, 3.7
- In
-
6.2 Verify the
/summaryportion of bug condition exploration test now passes- Property 1: Expected Behavior -
/summaryDoes Not Silently Drop Sibling Uploads - IMPORTANT: Re-run the SAME test case 1.D from task 1 — do NOT write a new test
- Run
npm run test:backend -- compliance-duplicate-chart-entries.property.test.js -t "/summary" - EXPECTED OUTCOME: Test case 1.D PASSES (confirms
/summarydiscloses sibling uploads viamulti_vertical_uploads) - Requirements: Property 4 (Validates 2.8, 2.9) from design
- Property 1: Expected Behavior -
-
-
7. Fix
persistUpload()snapshot block — filter and group byvertical-
7.1 Rewrite the
verticalStatsquery to filter by the upload'svertical- In
backend/routes/compliance.jspersistUpload()(lines 81–192), at theverticalStatsquery around line 157, capture the upload'sverticalfrom the row returned by theRETURNING idinsert (or accept it as apersistUpload()parameter) - Replace the
verticalStatsquery with the SQL from design.md Fix 5: filterWHERE team IS NOT NULL AND vertical IS NOT DISTINCT FROM $1and group by(vertical, team). TheIS NOT DISTINCT FROMoperator handles the legacyvertical IS NULLcase so AEO-only uploads keep their previous semantics - Leave the existing
INSERT ... ON CONFLICT (snapshot_month, vertical) DO UPDATEmapping as-is socompliance_snapshotsconsumers (/vcl/stats) continue to read the same column shape; only the underlying counts change - Keep snapshot creation wrapped in the existing
try/catchso a snapshot failure is logged and does not fail the upload commit - Bug_Condition: isBugCondition for
persistUpload()iscompliance_itemscontaining rows for verticals other than the upload's vertical — the unfiltered query inflatestotal_devices/compliant/non_compliant - Expected_Behavior: compliance_snapshots rows written by persistUpload() have
total_devices,compliant,non_compliantderived only from compliance_items rows belonging to the snapshotted vertical; no item from another vertical contributes (Property 5 from design) - Preservation: Single-vertical months produce identical snapshot rows; the legacy
vertical IS NULLAEO-only path is unchanged viaIS NOT DISTINCT FROM; the snapshot try/catch error path is unchanged - Requirements: 2.10, 2.11, 3.8, 3.9
- In
-
7.2 Verify the
persistUpload()portion of bug condition exploration test now passes- Property 1: Expected Behavior - persistUpload() Snapshot Reflects Only the Snapshotted Vertical
- IMPORTANT: Re-run the SAME test case 1.E from task 1 — do NOT write a new test
- Run
npm run test:backend -- compliance-duplicate-chart-entries.property.test.js -t "persistUpload" - EXPECTED OUTCOME: Test case 1.E PASSES (confirms snapshot rows are filtered to the snapshotted vertical only)
- Requirements: Property 5 (Validates 2.10, 2.11) from design
-
-
8. Verify preservation tests still pass after all five fixes
- Property 2: Preservation - Single-Upload-Per-Date Behavior Unchanged
- IMPORTANT: Re-run the SAME tests from task 2 — do NOT write new tests
- Run preservation property tests from task 2:
npm run test:backend -- compliance-duplicate-chart-entries.property.test.js -t "Preservation" - Confirm all six preservation test cases (2.A–2.F) and the cross-endpoint property-based preservation extension still pass
- EXPECTED OUTCOME: Tests PASS (confirms no regressions across the four read endpoints, the
/summaryteamfilter, thepersistUpload()single-vertical-month path, and the snapshot error-path behavior) - Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 3.10
-
9. Checkpoint — Ensure all tests pass
- Run the full backend test suite:
npm run test:backend - Confirm
compliance-duplicate-chart-entries.property.test.jspasses end-to-end (both Property 1 expected-behavior cases and Property 2 preservation cases) - Confirm pre-existing tests (
vcl-compliance-reporting.property.test.js,vcl-aggregated-burndown.property.test.js,vcl-aggregated-burndown.test.js,vcl-compliance-reporting.test.js,fp-submissions-cleanup.test.js, etc.) still pass — none of these should be affected since the fix is contained to read queries and one snapshot write query incompliance.js - Spot-check the integration scenarios from design.md "Integration Tests": upload three xlsx files for the same
report_dateviaPOST /preview+POST /commit, then call/trends,/top-recurring,/category-trend,/summaryand verify aggregated/disclosed responses; call/vcl/statsand verify per-verticalcompliance_pctis correct - Ensure all tests pass, ask the user if questions arise
- Run the full backend test suite:
Notes
- All five fixes are contained to
backend/routes/compliance.js. No database migration, no new column, no frontend change. - The Property 1 / Property 2 task numbering follows the bugfix workflow convention so the IDE hover-status indicator can track exploration tests against expected-behavior verification. Task 1 is the single Property 1 source; tasks 3.2 / 4.2 / 5.2 / 6.2 / 7.2 each re-run the relevant slice of Property 1 (NOT new tests) to confirm the matching fix lands correctly. Task 2 is the single Property 2 source; task 8 re-runs Property 2 in full to confirm no regressions.
- The implementation tasks (3 through 7) are independent at the SQL level. Each can be reviewed and merged without waiting on the others, as long as task 1 and task 2 have run on the unfixed code first.
- The
_Bug_Condition,_Expected_Behavior, and_Preservationannotations on each fix sub-task reference the formal pseudocode indesign.mdGlossary and Bug Details sections. _Requirements: X.Y_annotations cite clauses inbugfix.mdBug Analysis.