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
21 KiB
Design Document
Overview
This feature surfaces each noncompliant metric's estimated resolution date at the top of that metric's section in the asset sidebar (ComplianceDetailPanel.js) on the AEO Compliance page. It is a read and display oriented change confined to the React rendering layer. No backend, API, database, or migration work is required: the per-metric resolution_date field is already returned by GET /api/compliance/items/:hostname inside detail.metrics[], and it is already editable through the existing Resolution Date metadata field and PATCH /api/compliance/items/:hostname/metadata.
Today the only place a user sees a resolution date is the editable Resolution Date metadata input lower in the sidebar. That input applies to the currently selected metrics and collapses to "Multiple values" whenever the selected metrics disagree, so a viewer cannot see each metric's own projected compliance date at a glance. This feature adds a read-only, per-metric date line to the top of every failing (status === 'active') MetricRow, drawn from that same metric's resolution_date, formatted as YYYY-MM-DD, with explicit placeholders for the no-date and invalid-date cases. The existing editable metadata field and its "Multiple values" logic are left unchanged.
The only non-trivial logic is date interpretation: deciding whether a stored resolution_date is a valid calendar date, normalising it to YYYY-MM-DD, and distinguishing "no date set" from "a value is present but it is not a valid date." That logic is extracted into a small pure helper so it can be unit- and property-tested independently of React.
Scope summary:
- Add a pure date helper (
frontend/src/utils/resolutionDate.js) that classifies and formats a rawresolution_datevalue. - Render a read-only estimated-resolution-date line at the top of each active
MetricRow, above the metric description and all other fields. - Suppress that line for resolved metrics.
- Leave the editable Resolution Date metadata
Section,computeSharedValues,handleSaveMetadata, and the metadata PATCH flow untouched.
Architecture
The change lives entirely in the frontend rendering layer. The data already flows from the compliance API into ComplianceDetailPanel; this feature only adds a derived, read-only presentation of a field that is already in hand.
flowchart TD
API["GET /api/compliance/items/:hostname<br/>detail.metrics[].resolution_date"] --> Panel["ComplianceDetailPanel<br/>(state: detail)"]
Panel --> Split["activeMetrics = status 'active'<br/>resolvedMetrics = status 'resolved'"]
Split --> Row["MetricRow({ metric, resolved })"]
Row --> Helper["formatResolutionDate(metric.resolution_date)<br/>(pure helper, utils/resolutionDate.js)"]
Helper --> Decide{"resolved?"}
Decide -->|"yes"| Skip["render existing row only<br/>(no estimated date line)"]
Decide -->|"no"| Display["render read-only date line at top of section:<br/>set -> 'YYYY-MM-DD'<br/>none -> 'not set' placeholder<br/>invalid -> 'invalid' placeholder"]
Panel --> Meta["Resolution Date metadata Section<br/>(input + computeSharedValues + handleSaveMetadata)<br/>UNCHANGED"]
Meta --> Patch["PATCH /api/compliance/items/:hostname/metadata"]
Patch -.->|"on success: fetchDetail() re-reads detail"| Panel
Key architectural decisions:
- Pure helper for all date logic. Parsing, validation, and formatting are isolated in
formatResolutionDate, a pure function with no React, no I/O, and no dependence on the system clock, timezone, or locale. This keeps the rendering code declarative and makes the only branching logic in the feature directly testable. Rationale:MetricRowis a presentational function component; mixing date parsing into JSX would be untestable without rendering, andnew Date(...).toLocaleDateString()would make output depend on the runner's timezone and locale, which theYYYY-MM-DDrequirement forbids. - Display derives from the live
detailstate. The date line readsmetric.resolution_datefrom the samedetail.metrics[]array that feeds the editable metadata field. BecausehandleSaveMetadataalready callsfetchDetail()on a successful save, the displayed value updates automatically after an edit with no extra wiring. Rationale: satisfies Requirements 4.2, 4.3, and 4.5 without duplicating state or adding a second source of truth. - No new state, no shared/collapsed value. The per-metric line is computed inline per
MetricRowfrom that row's own metric object. It deliberately does not usecomputeSharedValues, so two metrics with different dates each show their own value and never collapse to "Multiple values." Rationale: Requirements 3.2 and 3.3. - Read-only by construction. The new element is plain text (a label plus a value) with no
input,button,a, or change handler, so it is identical and non-interactive for every role. Rationale: Requirements 5.1–5.4. Role-based gating of the existing edit field is unchanged and out of scope.
Components and Interfaces
frontend/src/utils/resolutionDate.js (new)
A pure helper module, following the established frontend/src/utils/ pattern (for example queueGrouping.js). It exports a single classification-and-formatting function and the placeholder strings, so both the component and the tests reference the same constants.
/**
* Classify and format a raw per-metric resolution_date value for display.
*
* Spec: .kiro/specs/compliance-metric-estimated-resolution-date
* Requirements: 1.1, 1.4, 1.6, 2.1
*
* Pure and deterministic: the result depends only on `raw`. It does not read
* the system clock, timezone, or locale. Validation is strict YYYY-MM-DD with
* a real-calendar-date check (correct month lengths and leap years), which
* matches how the value is produced by the <input type="date"> editor.
*
* @param {string|null|undefined} raw - the metric's resolution_date field
* @returns {{ state: 'set', value: string } | { state: 'none' } | { state: 'invalid' }}
* - { state: 'set', value } the value is a valid calendar date; `value` is YYYY-MM-DD
* - { state: 'none' } the value is null, undefined, empty, or whitespace-only
* - { state: 'invalid' } the value is non-empty but not a valid calendar date
*/
export function formatResolutionDate(raw) { /* ... */ }
// Display constants (single source of truth for component + tests)
export const RESOLUTION_DATE_LABEL = 'Est. Resolution';
export const NO_DATE_PLACEHOLDER = 'not set';
export const INVALID_DATE_PLACEHOLDER = 'invalid date';
Classification rules:
| Input condition | Result |
|---|---|
null or undefined |
{ state: 'none' } |
empty string or whitespace-only (after trim()) |
{ state: 'none' } |
trimmed value matches ^\d{4}-\d{2}-\d{2}$ and is a real calendar date |
{ state: 'set', value: '<YYYY-MM-DD>' } |
any other non-empty value (wrong shape, 2026-13-01, 2026-02-30, not-a-date) |
{ state: 'invalid' } |
Validation approach: the helper trims, tests against the strict ^\d{4}-\d{2}-\d{2}$ shape, then verifies the year/month/day form an actual calendar date (month 1–12, day within that month's length, leap-year aware). It avoids new Date(string) for the validity decision because that constructor accepts many non-YYYY-MM-DD shapes and applies timezone offsets, which would make a date-only field ambiguous. Because the editable field is an <input type="date">, well-formed stored values are already YYYY-MM-DD; the strict check simply normalises and defends against legacy or malformed values per Requirement 1.6.
MetricRow({ metric, resolved, onNavigate }) (modified)
MetricRow gains a single read-only block rendered as the first child of the row's content, before the existing top row (MetricChip + resolved label), the metric description, the Ivanti ID row, and the highlights list.
- The block renders only when
resolvedis falsy. For resolved metrics (resolved === true),MetricRowbehaves exactly as today (Requirements 3.1, 3.4). - For active metrics, it calls
formatResolutionDate(metric.resolution_date)and renders, per the returnedstate:set— the label and theYYYY-MM-DDvalue (Requirements 1.1, 1.4, 1.5).none— the label and theNO_DATE_PLACEHOLDERtext (Requirements 2.1, 4.5).invalid— the label and theINVALID_DATE_PLACEHOLDERtext; the remainder of the row still renders (Requirement 1.6).
- The block is positioned at the top of the section using the existing row layout, so it sits above
metric_descand all supplementary fields (Requirement 1.2). - Every active metric renders this block regardless of the
resolution_datestate, andMetricRowis still invoked once per metric inactiveMetrics.map(...), so all noncompliant metrics continue to render (Requirement 2.2).
No prop signature change is required: MetricRow already receives the full metric object, which carries resolution_date. The resolved prop already distinguishes the two call sites.
ComplianceDetailPanel (unchanged)
The container is not modified. The two MetricRow call sites remain:
activeMetrics.map(m => <MetricRow key={m.metric_id} metric={m} onNavigate={onNavigate} />)— active metrics show the date line.resolvedMetrics.map(m => <MetricRow key={m.metric_id} metric={m} resolved />)— resolved metrics do not.
The Resolution Date metadata Section, the resolutionDate state, computeSharedValues, handleSaveMetadata, the "Multiple values" placeholder logic, and the metadata PATCH call are all left exactly as they are (Requirement 4.1). Because handleSaveMetadata already re-runs fetchDetail() after a successful save, the new display reflects edits automatically (Requirements 4.2, 4.3); on a failed save the catch block sets metaError and never updates detail, so the previously displayed value is retained (Requirement 4.4).
Data Models
No new persisted data models are introduced. The feature consumes the existing metric shape returned by the compliance API.
Metric object (existing, as returned in detail.metrics[]):
| Field | Type | Relevance to this feature |
|---|---|---|
metric_id |
string (e.g. "2.3.6i") |
row key; identifies the metric |
category |
string | drives categoryColor accent (existing) |
status |
'active' | 'resolved' |
active = noncompliant (show date line); resolved = compliant (suppress) |
metric_desc |
string | existing description; date line renders above it |
resolution_date |
string | null | source value for the estimated resolution date display |
remediation_plan |
string | null | unrelated; consumed by existing metadata field |
seen_count, first_seen, resolved_on |
mixed | existing fields, unaffected |
extra |
object | existing highlights source, unaffected |
Helper result model (new, in-memory only):
// Discriminated union returned by formatResolutionDate(raw)
{ state: 'set', value: string } // value is normalised 'YYYY-MM-DD'
{ state: 'none' } // null / empty / whitespace-only
{ state: 'invalid' } // present but not a valid calendar date
This object exists only during render and is never persisted or sent to the API.
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.
The testable logic in this feature is concentrated in the pure helper formatResolutionDate(raw). The properties below are universally quantified over its input space and over lists of metrics. Acceptance criteria that concern fixed DOM placement, label presence, role-independent rendering, conditional suppression for resolved metrics, and the save/refetch round-trip are not universal properties of varying input; they are covered by example, render, and integration tests in the Testing Strategy.
The prework consolidated nine property-amenable criteria into five non-redundant properties: criteria 1.1 and 1.4 became Property 1; criteria 2.1 and 4.5 became Property 2; criterion 1.6 became Property 3; criterion 2.2 became Property 4; and criteria 1.3, 3.2, and 3.3 became Property 5.
Property 1: Valid calendar dates classify as "set" and format as YYYY-MM-DD
For any string that is a valid calendar date in YYYY-MM-DD form (correct month range, day within the month's true length, leap-year aware), formatResolutionDate returns { state: 'set', value } where value is the canonical zero-padded YYYY-MM-DD string (matching ^\d{4}-\d{2}-\d{2}$) equal to the input's normalized form.
Validates: Requirements 1.1, 1.4
Property 2: Absent values classify as "none"
For any input that is null, undefined, the empty string, or a string composed entirely of whitespace, formatResolutionDate returns { state: 'none' }.
Validates: Requirements 2.1, 4.5
Property 3: Non-empty non-calendar-date values classify as "invalid"
For any non-empty, non-whitespace-only string that is not a valid YYYY-MM-DD calendar date — including wrong shapes, out-of-range months or days, impossible calendar days such as 2026-02-30, and arbitrary text — formatResolutionDate returns { state: 'invalid' }.
Validates: Requirements 1.6
Property 4: Classification is total over any metric list
For any array of metrics whose resolution_date fields are drawn from all input categories (valid dates, null, empty, whitespace-only, and malformed strings), formatResolutionDate applied to each metric's field never throws and returns exactly one state in { 'set', 'none', 'invalid' } for every metric, and the number of classified results equals the number of metrics.
Validates: Requirements 2.2
Property 5: Each metric's display derives only from its own field (no collapsing)
For any array of metrics — including arrays where two or more metrics have different resolution_date values — the derived estimated-resolution display for each metric equals formatResolutionDate applied to that same metric's own resolution_date, independent of every other metric's value, and no result is replaced by a shared or "Multiple values" representation.
Validates: Requirements 1.3, 3.2, 3.3
Error Handling
Because the feature is read-only rendering over data already in component state, error handling is about defensive classification rather than failure recovery.
| Condition | Handling |
|---|---|
resolution_date is null / undefined |
formatResolutionDate returns { state: 'none' }; the row shows the no-date placeholder (Requirement 2.1). |
resolution_date is empty or whitespace-only |
Trimmed to empty → { state: 'none' }; no-date placeholder. |
resolution_date is present but malformed or not a real calendar date |
{ state: 'invalid' }; the row shows the invalid placeholder and continues rendering the description and all other fields (Requirement 1.6). The helper never throws on bad input. |
Metric object missing resolution_date entirely |
Accessing an absent property yields undefined, classified as { state: 'none' }. |
| Metadata save fails (existing flow) | The existing handleSaveMetadata catch sets metaError and does not mutate detail, so the previously displayed estimated resolution date is retained and an error is shown (Requirement 4.4). No change to this behavior. |
| Detail fetch fails (existing flow) | The existing error state renders the panel-level error block; no metric rows (and therefore no date lines) render. Unchanged. |
The helper is the single guard point: every branch of its discriminated union maps to a defined render path, so no metric input can crash MetricRow or leave a row partially rendered.
Testing Strategy
This feature uses both property-based tests (for the pure helper's universal behavior) and example/render tests (for fixed DOM structure, conditional suppression, role-independence, and the save round-trip). Property-based testing is appropriate here because formatResolutionDate is a pure function over a large input space (arbitrary strings, null, whitespace, malformed and valid dates) with clear universal invariants. It is not appropriate for the placement, label-presence, read-only-structure, and save/refetch criteria, which are fixed behaviors verified more directly by example.
Property-based tests (helper — frontend/src/utils/__tests__/resolutionDate.property.test.js):
- Library:
fast-checkwith Jest (react-scripts test/@testing-library), matching existing*.property.test.jsfiles such asqueue-grouping.property.test.jsandmetricDefinitions.property.test.js. - Implemented from scratch is prohibited; use
fast-checkarbitraries. - Each property runs a minimum of 100 iterations (
{ numRuns: 100 }or higher, consistent with existing tests that use 100–200). - Each test is tagged with a comment referencing its design property, in the format: Feature: compliance-metric-estimated-resolution-date, Property {number}: {property_text}.
- Implement each of the five correctness properties with a single property-based test:
- Property 1 — generator: valid calendar dates spanning years, all months, month-length boundaries (28/29/30/31), and leap days; assert
state === 'set'andvaluematches^\d{4}-\d{2}-\d{2}$and equals the canonical form. - Property 2 — generator:
fc.constantFrom(null, undefined, '')combined with whitespace-only strings built from spaces, tabs, and newlines of varying length; assertstate === 'none'. - Property 3 — generator: non-empty 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'. - Property 4 — generator: arrays mixing all categories; assert no throw, each result's
stateis in{ 'set', 'none', 'invalid' }, and result count equals input length. - Property 5 — 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-equalsformatResolutionDate(metric.resolution_date)computed in isolation, and that no result is a "Multiple values" sentinel.
- Property 1 — generator: valid calendar dates spanning years, all months, month-length boundaries (28/29/30/31), and leap days; assert
Example and edge-case unit tests (helper):
- Concrete fixtures that anchor the contract and double as regression cases:
'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.
Example / render tests (component — MetricRow / ComplianceDetailPanel):
- Using
@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): the
RESOLUTION_DATE_LABELtext appears adjacent to the value for an active row with a valid date. - No-date and invalid placeholders (2.1, 1.6): active rows with empty and malformed dates render the respective placeholder text and still render the metric description.
- 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.
- Existing editor preserved (4.1): the panel still renders the editable Resolution Date
input[type=date]. - 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 the new display introduces no editing control.
Integration / interaction tests (existing save flow):
- 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.
Build and lint verification:
- After implementation, run
cd frontend && npm run buildto confirm the production build compiles and ESLint warnings stay within the 25-warning budget. Prefix any intentionally-unused variables with_per the project lint rules. - Run
cd frontend && CI=true npm test(non-watch) to execute the new property and example tests alongside the existing suite.