# 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 raw `resolution_date` value. - 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. ```mermaid flowchart TD API["GET /api/compliance/items/:hostname
detail.metrics[].resolution_date"] --> Panel["ComplianceDetailPanel
(state: detail)"] Panel --> Split["activeMetrics = status 'active'
resolvedMetrics = status 'resolved'"] Split --> Row["MetricRow({ metric, resolved })"] Row --> Helper["formatResolutionDate(metric.resolution_date)
(pure helper, utils/resolutionDate.js)"] Helper --> Decide{"resolved?"} Decide -->|"yes"| Skip["render existing row only
(no estimated date line)"] Decide -->|"no"| Display["render read-only date line at top of section:
set -> 'YYYY-MM-DD'
none -> 'not set' placeholder
invalid -> 'invalid' placeholder"] Panel --> Meta["Resolution Date metadata Section
(input + computeSharedValues + handleSaveMetadata)
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: `MetricRow` is a presentational function component; mixing date parsing into JSX would be untestable without rendering, and `new Date(...).toLocaleDateString()` would make output depend on the runner's timezone and locale, which the `YYYY-MM-DD` requirement forbids. - **Display derives from the live `detail` state.** The date line reads `metric.resolution_date` from the same `detail.metrics[]` array that feeds the editable metadata field. Because `handleSaveMetadata` already calls `fetchDetail()` 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 `MetricRow` from that row's own metric object. It deliberately does not use `computeSharedValues`, 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. ```javascript /** * 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 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: '' }` | | 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 ``, 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 `resolved` is falsy. For resolved metrics (`resolved === true`), `MetricRow` behaves exactly as today (Requirements 3.1, 3.4). - For active metrics, it calls `formatResolutionDate(metric.resolution_date)` and renders, per the returned `state`: - `set` — the label and the `YYYY-MM-DD` value (Requirements 1.1, 1.4, 1.5). - `none` — the label and the `NO_DATE_PLACEHOLDER` text (Requirements 2.1, 4.5). - `invalid` — the label and the `INVALID_DATE_PLACEHOLDER` text; 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_desc` and all supplementary fields (Requirement 1.2). - Every active metric renders this block regardless of the `resolution_date` state, and `MetricRow` is still invoked once per metric in `activeMetrics.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 => )` — active metrics show the date line. - `resolvedMetrics.map(m => )` — 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):** ```javascript // 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-check` with Jest (`react-scripts test` / `@testing-library`), matching existing `*.property.test.js` files such as `queue-grouping.property.test.js` and `metricDefinitions.property.test.js`. - Implemented from scratch is prohibited; use `fast-check` arbitraries. - 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'` and `value` matches `^\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; assert `state === 'none'`. - Property 3 — generator: non-empty strings that are not valid `YYYY-MM-DD` dates (wrong shapes, month `00`/`13`+, day `00`/`32`+, `2026-02-30`, arbitrary text), filtered to exclude any accidentally-valid date; assert `state === 'invalid'`. - Property 4 — generator: arrays mixing all categories; assert no throw, each result's `state` is in `{ 'set', 'none', 'invalid' }`, and result count equals input length. - Property 5 — generator: arrays of metric-like objects with independently chosen `resolution_date` values (including arrays forced to contain differing dates); assert each metric's derived result deep-equals `formatResolutionDate(metric.resolution_date)` computed in isolation, and that no result is a "Multiple values" sentinel. **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_LABEL` text 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 `PATCH` followed by `fetchDetail` returning 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`; assert `detail` is unmodified (previously displayed date retained) and an error indication is shown. **Build and lint verification:** - After implementation, run `cd frontend && npm run build` to 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.