New specs: archer-template-library, ccp-metrics-view-restructure, compliance-list-stale-after-sidebar-edit, compliance-metric-estimated-resolution-date, compliance-remediation-display-fix, flexible-jira-ticket-creation, forecast-burndown-chart, granite-loader-export, ivanti-queue-clear-completed-fix, multi-item-jira-ticket, queue-collapsible-sections, vendor-issue-type-dropdown New steering: archer-template-gen.md Updated: migration-registration-check hook, remediation-plan-history spec, gitlab-workflow, tech, versioning steering files
12 KiB
Implementation Plan: Compliance Metric Estimated Resolution Date
Overview
This plan implements a read-only, per-metric estimated resolution date line at the top of each noncompliant metric's section in the asset sidebar (ComplianceDetailPanel.js). The work is frontend-only (React 19, plain JavaScript) and is built test-first: the pure date helper and its property-based tests come first, followed by the MetricRow rendering change and its render tests, then build and test verification.
All date logic is isolated in a pure helper (frontend/src/utils/resolutionDate.js) so it can be property- and example-tested independently of React. The component change adds a single read-only block to MetricRow and changes no prop signatures. The editable Resolution Date metadata Section, computeSharedValues, handleSaveMetadata, and the metadata PATCH flow are left unchanged.
Tasks
-
1. Create the pure resolution-date helper module
-
1.1 Implement
formatResolutionDateand display constants infrontend/src/utils/resolutionDate.js- Create
frontend/src/utils/resolutionDate.jsfollowing the existingfrontend/src/utils/pure-module pattern (for examplequeueGrouping.js) - Export
formatResolutionDate(raw)returning the discriminated union{ state: 'set', value } | { state: 'none' } | { state: 'invalid' } - Return
{ state: 'none' }fornull,undefined, empty string, or whitespace-only (aftertrim()) - Return
{ state: 'set', value }only when the trimmed value matches^\d{4}-\d{2}-\d{2}$AND is a real calendar date (month1–12, day within the month's true length, leap-year aware);valueis the normalizedYYYY-MM-DDstring - Return
{ state: 'invalid' }for any other non-empty value (wrong shape,2026-13-01,2026-02-30, arbitrary text) - Keep the function pure and deterministic: no React, no I/O, no system clock/timezone/locale dependency, and do NOT use
new Date(string)for the validity decision - Export
RESOLUTION_DATE_LABEL = 'Est. Resolution',NO_DATE_PLACEHOLDER = 'not set', andINVALID_DATE_PLACEHOLDER = 'invalid date'as the single source of truth for component and tests - Requirements: 1.1, 1.4, 1.6, 2.1
- Create
-
* 1.2 Write property test for valid calendar date classification and formatting
- File:
frontend/src/utils/__tests__/resolutionDate.property.test.js - Use
fast-check(v4) with Jest (react-scripts test); do not hand-roll generators - Property 1: Valid calendar dates classify as "set" and format as YYYY-MM-DD
- Generator: valid calendar dates spanning years, all months, month-length boundaries (28/29/30/31), and leap days; assert
state === 'set',valuematches^\d{4}-\d{2}-\d{2}$and equals the canonical normalized form - Run a minimum of 100 iterations (
{ numRuns: 100 }or higher) - Tag the test:
// Feature: compliance-metric-estimated-resolution-date, Property 1: Valid calendar dates classify as "set" and format as YYYY-MM-DD - Validates: Requirements 1.1, 1.4
- File:
-
* 1.3 Write property test for absent values classifying as "none"
- File:
frontend/src/utils/__tests__/resolutionDate.property.test.js - Property 2: Absent values classify as "none"
- Generator:
fc.constantFrom(null, undefined, '')combined with whitespace-only strings built from spaces, tabs, and newlines of varying length; assertstate === 'none' - Minimum 100 iterations
- Tag the test:
// Feature: compliance-metric-estimated-resolution-date, Property 2: Absent values classify as "none" - Validates: Requirements 2.1, 4.5
- File:
-
* 1.4 Write property test for non-calendar-date values classifying as "invalid"
- File:
frontend/src/utils/__tests__/resolutionDate.property.test.js - Property 3: Non-empty non-calendar-date values classify as "invalid"
- Generator: non-empty, non-whitespace-only strings that are not valid
YYYY-MM-DDdates (wrong shapes, month00/13+, day00/32+,2026-02-30, arbitrary text), filtered to exclude any accidentally-valid date; assertstate === 'invalid' - Minimum 100 iterations
- Tag the test:
// Feature: compliance-metric-estimated-resolution-date, Property 3: Non-empty non-calendar-date values classify as "invalid" - Validates: Requirements 1.6
- File:
-
* 1.5 Write property test for total classification over any metric list
- File:
frontend/src/utils/__tests__/resolutionDate.property.test.js - Property 4: Classification is total over any metric list
- Generator: arrays mixing all input categories (valid dates,
null, empty, whitespace-only, malformed); assertformatResolutionDatenever throws, each result'sstateis in{ 'set', 'none', 'invalid' }, and the result count equals the input length - Minimum 100 iterations
- Tag the test:
// Feature: compliance-metric-estimated-resolution-date, Property 4: Classification is total over any metric list - Validates: Requirements 2.2
- File:
-
* 1.6 Write property test for per-metric independence (no collapsing)
- File:
frontend/src/utils/__tests__/resolutionDate.property.test.js - Property 5: Each metric's display derives only from its own field (no collapsing)
- Generator: arrays of metric-like objects with independently chosen
resolution_datevalues (including arrays forced to contain differing dates); assert each metric's derived result deep-equalsformatResolutionDateapplied to that same metric's ownresolution_datecomputed in isolation, and that no result is replaced by a shared/"Multiple values" sentinel - Minimum 100 iterations
- Tag the test:
// Feature: compliance-metric-estimated-resolution-date, Property 5: Each metric's display derives only from its own field (no collapsing) - Validates: Requirements 1.3, 3.2, 3.3
- File:
-
* 1.7 Write example and edge-case unit tests for the helper
- File:
frontend/src/utils/__tests__/resolutionDate.test.js - Concrete fixtures:
'2026-07-01'→{ state: 'set', value: '2026-07-01' };'2026-7-1'→invalid(not zero-padded);'07/01/2026'→invalid;'2024-02-29'→set(leap year);'2023-02-29'→invalid;' '→none;null→none - Requirements: 1.1, 1.4, 1.6, 2.1
- File:
-
-
2. Checkpoint - Ensure helper tests pass
- Run
cd frontend && CI=true npm test -- resolutionDateto confirm the helper and its property tests pass - Ensure all tests pass, ask the user if questions arise.
- Run
-
3. Render the estimated-resolution-date line in
MetricRow-
3.1 Add the read-only date block to
MetricRowinfrontend/src/components/pages/ComplianceDetailPanel.js- Import
formatResolutionDate,RESOLUTION_DATE_LABEL,NO_DATE_PLACEHOLDER, andINVALID_DATE_PLACEHOLDERfrom../../utils/resolutionDate - Render the new block as the first child of the row content, above the existing top row (
MetricChip), the metric description, the Ivanti ID row, and the highlights list (Requirement 1.2) - Render the block only when
resolvedis falsy; forresolved === true,MetricRowmust behave exactly as today (Requirements 3.1, 3.4) - For active metrics, call
formatResolutionDate(metric.resolution_date)and render by state:set→ label +YYYY-MM-DDvalue;none→ label +NO_DATE_PLACEHOLDER;invalid→ label +INVALID_DATE_PLACEHOLDER, with the rest of the row still rendering - Render
RESOLUTION_DATE_LABELas a visible text label adjacent to the value/placeholder (Requirement 1.5) - Use plain text only: no
input,button,a, or change handler in the new subtree (Requirements 5.3, 5.4) - Do NOT change the
MetricRowprop signature; readresolution_datefrom the existingmetricobject - Leave
computeSharedValues,handleSaveMetadata, the editable Resolution Date metadataSection, and the metadata PATCH flow unchanged (Requirement 4.1) - Prefix any intentionally-unused variables with
_per the project lint rules - Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 2.1, 2.2, 3.1, 3.2, 3.3, 3.4, 4.1, 5.3, 5.4
- Import
-
* 3.2 Write render tests for placement, labels, and placeholders
- File:
frontend/src/components/pages/__tests__/ComplianceDetailPanel.metricRow.test.jsusing@testing-library/react - Placement (1.2): an active row with a valid date renders the estimated-resolution element before the description in document order
- Label presence (1.5):
RESOLUTION_DATE_LABELtext appears adjacent to the value for an active row with a valid date - Set value (1.1, 1.4): an active row with
'2026-07-01'renders2026-07-01 - No-date placeholder (2.1, 4.5): active rows with
null/empty/whitespace renderNO_DATE_PLACEHOLDERand still render the metric description - Invalid placeholder (1.6): an active row with a malformed date renders
INVALID_DATE_PLACEHOLDERand still renders the metric description - Requirements: 1.1, 1.2, 1.4, 1.5, 1.6, 2.1, 4.5
- File:
-
* 3.3 Write render tests for resolved suppression, read-only structure, and role-independence
- File:
frontend/src/components/pages/__tests__/ComplianceDetailPanel.metricRow.test.js - Resolved suppression (3.1, 3.4): a resolved row with a populated date renders no estimated-resolution line; a mixed list shows the line only in active rows
- Read-only structure (5.3): the date-line subtree contains no
input,button,a, or change handler - Role-independence (5.1, 5.2, 5.4): rendering under viewer, editor, and admin auth contexts produces identical date-line output and introduces no editing control
- Existing editor preserved (4.1): the panel still renders the editable Resolution Date
input[type=date] - Requirements: 3.1, 3.4, 4.1, 5.1, 5.2, 5.3, 5.4
- File:
-
* 3.4 Write interaction tests for the existing save round-trip
- File:
frontend/src/components/pages/__tests__/ComplianceDetailPanel.metricRow.test.js - Successful save (4.2, 4.3, 4.5): mock a successful
PATCHfollowed byfetchDetailreturning updated metrics; assert the displayed value updates to the new date, and that clearing the field renders the no-date placeholder - Failed save (4.4): mock a failing
PATCH; assertdetailis unmodified (previously displayed date retained) and an error indication is shown - Requirements: 4.2, 4.3, 4.4, 4.5
- File:
-
-
4. Final checkpoint - Build and test verification
-
4.1 Verify production build and ESLint budget
- Run
cd frontend && npm run buildand confirm the build compiles - Confirm ESLint warnings stay within the 25-warning budget; prefix any intentionally-unused variables with
_ - Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 2.1, 2.2, 3.1, 3.2, 3.3, 3.4, 4.1
- Run
-
4.2 Run the full test suite
- Run
cd frontend && CI=true npm test(non-watch) to execute the new property and render tests alongside the existing suite - Ensure all tests pass, ask the user if questions arise.
- Run
-
Notes
- Tasks marked with
*are optional test sub-tasks and can be skipped for a faster MVP, but they validate the five correctness properties and the fixed-DOM acceptance criteria and are recommended. - Each task references specific requirements (and, for property tests, the design property number) for traceability.
- Test-driven ordering: the pure helper and its property tests (task 1) come before the component change (task 3) so the only branching logic is validated first.
- Property tests use
fast-checkwith a minimum of 100 iterations and are tagged with their feature and property number per the design's Testing Strategy. - Checkpoints (tasks 2 and 4) ensure incremental validation and a clean production build within the lint budget.
Task Dependency Graph
{
"waves": [
{ "id": 0, "tasks": ["1.1"] },
{ "id": 1, "tasks": ["1.2", "1.3", "1.4", "1.5", "1.6", "1.7", "3.1"] },
{ "id": 2, "tasks": ["3.2", "3.3", "3.4"] },
{ "id": 3, "tasks": ["4.1"] },
{ "id": 4, "tasks": ["4.2"] }
]
}