# Implementation Plan: Compliance Metric Grouping
## Overview
Consolidate the AEO Compliance page's metric health cards from one-per-summary-entry to one-per-metric-family. Add variant pills inside each grouped card, a hover tooltip with metric context (300ms delay), an info panel for full metric definitions, and a static `metricDefinitions.json` data file. All work is frontend-only — no backend changes needed. The `metricFilter` state changes from `string|null` to `string[]|null` to support filtering by all metric IDs in a family.
## Tasks
- [x] 1. Create metric definitions data file and install test dependencies
- [x] 1.1 Create `frontend/src/data/metricDefinitions.json`
- Create the `frontend/src/data/` directory
- Build the JSON array from the metric definitions table provided by the user (130+ rows, 14 fields each)
- Each entry must have all 14 keys: `metric_id`, `metric_title`, `asset_types`, `asset_types_in_scope`, `application_types_in_scope`, `environment_in_scope`, `status_in_scope`, `instance_types_in_scope`, `criticality_levels_in_scope`, `exclusions`, `special_conditions`, `data_sources_required`, `business_justification`, `notes`
- Use empty strings for optional fields with no value — never `null` or omitted keys
- Verify the file imports without error via a quick `JSON.parse` check
- _Requirements: 6.1, 6.2, 6.3, 6.5, 8.3, 8.4_
- [x] 1.2 Install `fast-check` as a dev dependency
- Run `npm install --save-dev fast-check` in `frontend/`
- Verify it appears in `package.json` devDependencies
- _Requirements: (testing infrastructure)_
- [x] 2. Implement pure helper functions and their tests
- [x] 2.1 Add `computeWorstStatus` and `groupByMetricFamily` helpers to CompliancePage.js
- Add `STATUS_SEVERITY` map: `{ 'Below 15% of Target': 0, 'Within 15% of Target': 1, 'Meets/Exceeds Target': 2 }`
- Implement `computeWorstStatus(statuses)` — returns the status with the lowest severity rank from a non-empty array
- Implement `groupByMetricFamily(allEntries, team)` — filters entries by team, groups by `metric_id`, returns array of `{ metricId, entries, category, target, worstStatus }` objects
- Export both functions for testing (named exports alongside the default CompliancePage export)
- Remove the existing `teamMetrics()` helper (replaced by `groupByMetricFamily`)
- _Requirements: 1.1, 1.2, 1.6, 3.1_
- [x] 2.2 Write property test: Grouping invariant — no entries lost or misplaced
- **Property 1: Grouping invariant — no entries lost or misplaced**
- Create test file `frontend/src/components/pages/__tests__/complianceGrouping.property.test.js`
- Generate random arrays of summary entry objects with varying `metric_id` and `team` values
- Verify: (a) every entry appears in exactly one group, (b) all entries within a group share the same `metric_id`, (c) total entries across groups equals team-filtered input count
- Use `fc.assert(property, { numRuns: 100 })`
- **Validates: Requirements 1.1, 1.2**
- [x] 2.3 Write property test: Worst-status computation follows severity ordering
- **Property 2: Worst-status computation follows severity ordering**
- Generate random non-empty arrays of status strings from `{"Below 15% of Target", "Within 15% of Target", "Meets/Exceeds Target"}`
- Verify the result is the status with the lowest severity rank present in the array
- If the array contains "Below 15% of Target", the result must be "Below 15% of Target"
- Use `fc.assert(property, { numRuns: 100 })`
- **Validates: Requirements 1.6, 3.1**
- [ ]* 2.4 Write property test: Device filtering with metric family includes all matching devices
- **Property 3: Device filtering with metric family includes all matching devices**
- Generate random device arrays (each with a `failing_metrics` array of `{ metric_id }` objects) and random filter ID arrays
- Verify the filtered result contains exactly those devices with at least one matching `metric_id` in the filter array
- Use `fc.assert(property, { numRuns: 100 })`
- **Validates: Requirements 1.8, 7.1, 7.2**
- [x] 3. Checkpoint — Verify helpers and property tests
- Ensure all tests pass, ask the user if questions arise.
- [x] 4. Redesign MetricHealthCard with variant pills and worst-status coloring
- [x] 4.1 Redesign `MetricHealthCard` to accept a family group
- Change props from `{ entry, active, onClick }` to `{ family, active, onClick, onInfoClick, definitionLookup }`
- `family` is `{ metricId, entries, category, target, worstStatus }`
- Display base `metricId` as card title and `category` from the family
- Display shared `target` percentage
- Use `worstStatus` color for card border, status pill text, and status dot
- When all variants meet/exceed target, show "OK" status indicator with success color
- Add `Info` icon (lucide-react) in the top-right corner with `stopPropagation` on click to call `onInfoClick(family.metricId)`
- _Requirements: 1.2, 1.3, 1.6, 1.7, 3.1, 3.2, 3.3, 5.1, 5.5_
- [x] 4.2 Implement `VariantPill` inline sub-component
- Render one pill per `entry` in `family.entries`
- Each pill shows the entry's `description` or `team` label and compliance percentage in monospace
- Background tint from the entry's individual status color at ~12% opacity
- Show a glow dot when the variant's status is not "Meets/Exceeds Target"
- Layout: `inline-flex` with `flexWrap: 'wrap'` on the parent container
- _Requirements: 1.4, 1.5, 2.1, 2.2, 2.3, 2.4_
- [x] 5. Update CompliancePage state and rendering to use grouped families
- [x] 5.1 Add new imports and build the definitions lookup map
- Import `{ Info }` from `lucide-react`
- Import `MetricInfoPanel` from `./MetricInfoPanel` (created in task 6)
- Import `metricDefinitionsRaw` from `../../data/metricDefinitions.json`
- Build `METRIC_DEFINITIONS` lookup object at module level keyed by `metric_id`
- _Requirements: 6.3, 6.4_
- [x] 5.2 Update state management and filter logic
- Change `metricFilter` from `string|null` to `string[]|null`
- Add new state: `infoMetric` (`string|null`) — which metric's info panel is open
- Add new state: `hoveredMetric` (`string|null`) — which metric is being hovered
- Add new ref: `hoverTimeoutRef` — for 300ms delay management
- Update `filteredDevices` filter from `m.metric_id === metricFilter` to `metricFilter.includes(m.metric_id)`
- _Requirements: 7.1, 7.2_
- [x] 5.3 Update card rendering to use `groupByMetricFamily`
- Replace `const metrics = teamMetrics(summary.entries, activeTeam)` with `const families = groupByMetricFamily(summary.entries, activeTeam)`
- Replace `metrics.map(entry => )` with `families.map(family => )`
- On card click: set `metricFilter` to `family.entries.map(e => e.metric_id)` (array of all IDs in the family), or clear if already active
- Active state check: compare `metricFilter` array contents against the family's metric IDs
- Pass `onInfoClick` handler that sets `infoMetric` state
- Pass `definitionLookup` as `METRIC_DEFINITIONS`
- _Requirements: 1.2, 1.8, 7.1, 7.3, 7.4, 7.5_
- [x] 6. Implement MetricInfoPanel component
- [x] 6.1 Create `frontend/src/components/pages/MetricInfoPanel.js`
- Props: `metricId`, `definition` (from lookup or null), `summaryEntries` (family entries for fallback), `onClose`
- Render overlay/slide-out panel with dark theme matching DESIGN_SYSTEM.md
- Close button (X icon, top-right)
- Metric title in h3 monospace
- Sections with monospace uppercase labels: Asset Types, Asset Types In Scope, Application Types In Scope, Environment In Scope, Status In Scope, Instance Types In Scope, Criticality Levels In Scope, Exclusions, Special Conditions, Data Sources Required, Business Justification, Notes
- Show "—" placeholder for empty string fields
- If `definition` is null: show "No detailed definition available" with summary description fallback
- Click outside or close button calls `onClose()`
- _Requirements: 5.2, 5.3, 5.4, 5.6, 5.7_
- [x] 7. Implement HoverTooltip inline in CompliancePage
- [x] 7.1 Add hover tooltip logic and rendering
- On `mouseEnter` on MetricHealthCard: set 300ms timeout, then set `hoveredMetric` to `family.metricId`
- On `mouseLeave`: clear timeout, set `hoveredMetric` to null
- Render tooltip when `hoveredMetric` matches a family — positioned near the card using `getBoundingClientRect()`
- Tooltip content: metric title, business justification, data sources required (from `METRIC_DEFINITIONS` lookup)
- Fall back to summary entry `description` when no definition exists
- Dark card background with subtle border and shadow per DESIGN_SYSTEM.md
- Tooltip must not interfere with card click behavior
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6_
- [x] 8. Checkpoint — Verify full UI integration
- Ensure all tests pass, ask the user if questions arise.
- [x] 9. Property tests for definitions data and lookup
- [x] 9.1 Write property test: Definition lookup returns correct entry or null
- **Property 4: Definition lookup returns correct entry or null**
- Create test file `frontend/src/components/pages/__tests__/metricDefinitions.property.test.js`
- Generate random arrays of metric definition objects with unique `metric_id` values
- Build lookup map, query with IDs from the array (expect hit) and IDs not in the array (expect miss)
- Use `fc.assert(property, { numRuns: 100 })`
- **Validates: Requirements 4.2, 4.6**
- [x] 9.2 Write property test: Detail panel renders all required definition fields
- **Property 5: Detail panel renders all required definition fields**
- Generate random metric definition objects with all 14 fields
- Extract the set of field keys that the MetricInfoPanel renders and verify all required keys are present
- Use `fc.assert(property, { numRuns: 100 })`
- **Validates: Requirements 5.3**
- [x] 9.3 Write property test: Definitions schema validation — all entries have required fields
- **Property 6: Definitions schema validation — all entries have required fields**
- Generate random arrays of metric definition objects
- Verify every entry has all 14 keys present, `metric_id` is a non-empty string, and optional fields (`exclusions`, `special_conditions`, `notes`) are strings (not null/undefined)
- Use `fc.assert(property, { numRuns: 100 })`
- **Validates: Requirements 6.2, 8.3, 8.4**
- [x] 9.4 Write property test: Lookup map construction preserves all definitions
- **Property 7: Lookup map construction preserves all definitions**
- Generate random definition arrays with unique `metric_id` values
- Build lookup map and verify map size equals array length, and every definition is retrievable by its `metric_id`
- Use `fc.assert(property, { numRuns: 100 })`
- **Validates: Requirements 6.4**
- [x] 9.5 Write property test: JSON round-trip preserves metric definition data
- **Property 8: JSON round-trip preserves metric definition data**
- Generate random metric definition objects with string values for all fields
- Round-trip through `JSON.stringify` then `JSON.parse` and verify deep equality
- Use `fc.assert(property, { numRuns: 100 })`
- **Validates: Requirements 8.1, 8.2**
- [x] 10. Final checkpoint — Ensure 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
- Property tests validate universal correctness properties from the design document using fast-check
- Unit tests validate specific examples and edge cases
- All styling follows the project convention of inline styles (no CSS modules or Tailwind)
- The `fast-check` library must be installed as a dev dependency before running property tests
- The `metricDefinitions.json` file contains 130+ rows — the user will provide the metric definitions table data for conversion
- `computeWorstStatus` and `groupByMetricFamily` are exported as named exports from CompliancePage.js for testability