Sync .kiro/ from master — v2.2.0 release batch
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
This commit is contained in:
@@ -0,0 +1 @@
|
||||
{"specId": "0962d00a-cfe6-4a5d-88c6-e9e6f220b1a0", "workflowType": "requirements-first", "specType": "feature"}
|
||||
@@ -0,0 +1,244 @@
|
||||
# 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<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: `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 <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 `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 => <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):**
|
||||
|
||||
```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.
|
||||
|
||||
<!-- Sections from Overview through Data Models complete. Prework on acceptance criteria precedes the Correctness Properties section below. -->
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,81 @@
|
||||
# Requirements Document
|
||||
|
||||
## Introduction
|
||||
|
||||
This feature surfaces the estimated resolution date for each noncompliant metric directly at the top of that metric's section in the asset sidebar on the compliance page. Today, an asset's resolution date is only visible through the editable **Resolution Date** metadata field lower in the sidebar, where it is computed as a shared value across selected metrics and collapses to "Multiple values" when metrics disagree. This feature instead displays each metric's own `resolution_date` inline with the metric, so a viewer can see the projected compliance date (for example "2026-07-01") for metrics such as `2.3.6i`, `2.3.8i`, and `Vulns_Aging` without scrolling to or interacting with the metadata editor.
|
||||
|
||||
This is a read and display oriented feature. It surfaces the existing per-metric `resolution_date` value already tracked in the data model; it does not introduce new persistence, new editing surfaces, or changes to how the date is stored or computed.
|
||||
|
||||
Traceability: GitLab issue #20 — "[Feature Request] Show estimated resolution date in sidebar per metric" (http://steam-gitlab.charterlab.com/steam/cve-dashboard/-/issues/20).
|
||||
|
||||
## Glossary
|
||||
|
||||
- **Compliance_Page**: The AEO Compliance page in the frontend that lists noncompliant assets and their metrics.
|
||||
- **Asset_Sidebar**: The detail panel rendered by `ComplianceDetailPanel.js` that opens for a single asset (hostname) and lists that asset's metrics, metadata, history, and notes.
|
||||
- **Metric**: A single AEO compliance check tracked for an asset, identified by a `metric_id` (for example `2.3.6i`, `2.3.8i`, `Vulns_Aging`). Each metric has a `status` of `active` (noncompliant) or `resolved` (compliant).
|
||||
- **Noncompliant_Metric**: A metric with `status` equal to `active`.
|
||||
- **Compliant_Metric**: A metric with `status` equal to `resolved`.
|
||||
- **Estimated_Resolution_Date**: The projected date by which a metric is expected to return to compliance, stored per metric in the `resolution_date` field and derived from the per-metric remediation notes/date metadata.
|
||||
- **Resolution_Date_Field**: The existing per-metric `resolution_date` data field returned by the compliance API for each metric.
|
||||
- **Viewer**: A user with read-only access to all data.
|
||||
- **Editor**: A user with viewer permissions plus create and update operations.
|
||||
- **Admin**: A user with editor permissions plus delete, user management, and audit log access.
|
||||
- **Team**: A compliance business unit. Only STEAM and ACCESS-ENG are tracked.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1: Display estimated resolution date per noncompliant metric
|
||||
|
||||
**User Story:** As a viewer reviewing an asset's compliance posture, I want to see each noncompliant metric's estimated resolution date at the top of that metric's section, so that I can understand the projected compliance date for each metric without scrolling to or interacting with the metadata editor.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN the Asset_Sidebar renders a Noncompliant_Metric whose Resolution_Date_Field contains a value parseable as a calendar date, THE Asset_Sidebar SHALL display that metric's Estimated_Resolution_Date within that metric's section.
|
||||
2. WHEN the Asset_Sidebar renders a Noncompliant_Metric whose Resolution_Date_Field contains a value parseable as a calendar date, THE Asset_Sidebar SHALL position that metric's Estimated_Resolution_Date at the top of that metric's section, above the metric description and above all other fields belonging to that metric.
|
||||
3. THE Asset_Sidebar SHALL display the Estimated_Resolution_Date for each Noncompliant_Metric using the value of that same metric's Resolution_Date_Field.
|
||||
4. THE Asset_Sidebar SHALL display the Estimated_Resolution_Date as a calendar date in `YYYY-MM-DD` format (four-digit year, two-digit month, and two-digit day separated by hyphens).
|
||||
5. WHEN the Asset_Sidebar displays a Noncompliant_Metric's Estimated_Resolution_Date, THE Asset_Sidebar SHALL render a visible text label adjacent to that value identifying it as the estimated resolution date.
|
||||
6. IF a Noncompliant_Metric's Resolution_Date_Field is non-empty but does not contain a value parseable as a calendar date, THEN THE Asset_Sidebar SHALL omit the Estimated_Resolution_Date value for that metric and display a placeholder indicating that no valid estimated resolution date is available, while continuing to render the remainder of that metric's section.
|
||||
|
||||
### Requirement 2: Handle metrics with no estimated resolution date
|
||||
|
||||
**User Story:** As a viewer, I want a clear indication when a noncompliant metric has no estimated resolution date set, so that I can distinguish metrics with a plan from metrics that still need one.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. IF a Noncompliant_Metric has a Resolution_Date_Field that is null, an empty string, or contains only whitespace, THEN THE Asset_Sidebar SHALL display, at the top of that metric's section, a text placeholder that identifies the value as the estimated resolution date and indicates that no estimated resolution date is set for that metric.
|
||||
2. WHEN the Asset_Sidebar opens for an asset, THE Asset_Sidebar SHALL render every Noncompliant_Metric for that asset regardless of whether that metric's Resolution_Date_Field is populated, null, empty, or whitespace-only.
|
||||
|
||||
### Requirement 3: Scope display to noncompliant metrics
|
||||
|
||||
**User Story:** As a viewer, I want the estimated resolution date emphasized only for noncompliant metrics, so that the sidebar focuses attention on metrics that still require remediation.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. IF a Metric is a Compliant_Metric (status = resolved), THEN THE Asset_Sidebar SHALL NOT display the top-of-section Estimated_Resolution_Date (positioned above the metric description and supplementary fields) for that metric, regardless of whether that metric has a populated Resolution_Date_Field.
|
||||
2. WHEN the Asset_Sidebar renders the set of Noncompliant_Metric entries, THE Asset_Sidebar SHALL derive each metric's Estimated_Resolution_Date using only that same metric's Resolution_Date_Field value.
|
||||
3. WHEN two Noncompliant_Metric entries have different Resolution_Date_Field values, THE Asset_Sidebar SHALL display each metric's own Estimated_Resolution_Date without collapsing the values into a combined or "Multiple values" representation.
|
||||
4. WHEN the Asset_Sidebar renders an asset containing both Compliant_Metric and Noncompliant_Metric entries, THE Asset_Sidebar SHALL display the top-of-section Estimated_Resolution_Date only within Noncompliant_Metric sections.
|
||||
|
||||
### Requirement 4: Preserve existing metadata editing behavior
|
||||
|
||||
**User Story:** As an editor, I want the existing resolution date editing workflow to continue functioning unchanged, so that surfacing the per-metric date for display does not regress my ability to update it.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Asset_Sidebar SHALL continue to render the existing editable Resolution Date metadata field.
|
||||
2. WHEN an Editor successfully saves an updated Resolution_Date_Field for a Noncompliant_Metric, THE Asset_Sidebar SHALL display the updated Estimated_Resolution_Date for that metric within 2 seconds of the successful save.
|
||||
3. THE Asset_Sidebar SHALL derive the displayed Estimated_Resolution_Date from the same per-metric Resolution_Date_Field used by the existing metadata editing workflow.
|
||||
4. IF an Editor's save of an updated Resolution_Date_Field fails, THEN THE Asset_Sidebar SHALL retain the previously displayed Estimated_Resolution_Date and display an error indication for the failed save.
|
||||
5. WHEN an Editor clears the Resolution_Date_Field for a Noncompliant_Metric and the save succeeds, THE Asset_Sidebar SHALL display the no-date placeholder for that metric consistent with Requirement 2.
|
||||
|
||||
### Requirement 5: Role-based access to the display
|
||||
|
||||
**User Story:** As a security stakeholder, I want the estimated resolution date display to honor existing role-based access controls, so that read-only users can see the information without gaining editing capability.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHILE the current user is a Viewer, THE Asset_Sidebar SHALL display each Noncompliant_Metric's Estimated_Resolution_Date using the same value and `YYYY-MM-DD` format presented to Editor and Admin users.
|
||||
2. WHILE the current user is an Editor or Admin, THE Asset_Sidebar SHALL display each Noncompliant_Metric's Estimated_Resolution_Date using the same value and `YYYY-MM-DD` format presented to a Viewer.
|
||||
3. THE Asset_Sidebar SHALL present the top-of-section Estimated_Resolution_Date display as read-only content that contains no input field, button, link, or other interactive control capable of modifying a Resolution_Date_Field, regardless of the current user's role.
|
||||
4. WHILE the current user is a Viewer, THE Asset_Sidebar SHALL present no enabled control that creates or updates a Resolution_Date_Field value.
|
||||
138
.kiro/specs/compliance-metric-estimated-resolution-date/tasks.md
Normal file
138
.kiro/specs/compliance-metric-estimated-resolution-date/tasks.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# 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
|
||||
|
||||
- [x] 1. Create the pure resolution-date helper module
|
||||
- [x] 1.1 Implement `formatResolutionDate` and display constants in `frontend/src/utils/resolutionDate.js`
|
||||
- Create `frontend/src/utils/resolutionDate.js` following the existing `frontend/src/utils/` pure-module pattern (for example `queueGrouping.js`)
|
||||
- Export `formatResolutionDate(raw)` returning the discriminated union `{ state: 'set', value } | { state: 'none' } | { state: 'invalid' }`
|
||||
- Return `{ state: 'none' }` for `null`, `undefined`, empty string, or whitespace-only (after `trim()`)
|
||||
- Return `{ state: 'set', value }` only when the trimmed value matches `^\d{4}-\d{2}-\d{2}$` AND is a real calendar date (month `1–12`, day within the month's true length, leap-year aware); `value` is the normalized `YYYY-MM-DD` string
|
||||
- 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'`, and `INVALID_DATE_PLACEHOLDER = 'invalid date'` as the single source of truth for component and tests
|
||||
- _Requirements: 1.1, 1.4, 1.6, 2.1_
|
||||
|
||||
- [x]* 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'`, `value` matches `^\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**
|
||||
|
||||
- [x]* 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; assert `state === '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**
|
||||
|
||||
- [x]* 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-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'`
|
||||
- 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**
|
||||
|
||||
- [x]* 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); assert `formatResolutionDate` never throws, each result's `state` is 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**
|
||||
|
||||
- [x]* 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_date` values (including arrays forced to contain differing dates); assert each metric's derived result deep-equals `formatResolutionDate` applied to that same metric's own `resolution_date` computed 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**
|
||||
|
||||
- [x]* 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_
|
||||
|
||||
- [x] 2. Checkpoint - Ensure helper tests pass
|
||||
- Run `cd frontend && CI=true npm test -- resolutionDate` to confirm the helper and its property tests pass
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
|
||||
- [x] 3. Render the estimated-resolution-date line in `MetricRow`
|
||||
- [x] 3.1 Add the read-only date block to `MetricRow` in `frontend/src/components/pages/ComplianceDetailPanel.js`
|
||||
- Import `formatResolutionDate`, `RESOLUTION_DATE_LABEL`, `NO_DATE_PLACEHOLDER`, and `INVALID_DATE_PLACEHOLDER` from `../../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 `resolved` is falsy; for `resolved === true`, `MetricRow` must 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-DD` value; `none` → label + `NO_DATE_PLACEHOLDER`; `invalid` → label + `INVALID_DATE_PLACEHOLDER`, with the rest of the row still rendering
|
||||
- Render `RESOLUTION_DATE_LABEL` as 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 `MetricRow` prop signature; read `resolution_date` from the existing `metric` object
|
||||
- Leave `computeSharedValues`, `handleSaveMetadata`, the editable Resolution Date metadata `Section`, 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_
|
||||
|
||||
- [x]* 3.2 Write render tests for placement, labels, and placeholders
|
||||
- File: `frontend/src/components/pages/__tests__/ComplianceDetailPanel.metricRow.test.js` 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): `RESOLUTION_DATE_LABEL` text 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'` renders `2026-07-01`
|
||||
- No-date placeholder (2.1, 4.5): active rows with `null`/empty/whitespace render `NO_DATE_PLACEHOLDER` and still render the metric description
|
||||
- Invalid placeholder (1.6): an active row with a malformed date renders `INVALID_DATE_PLACEHOLDER` and still renders the metric description
|
||||
- _Requirements: 1.1, 1.2, 1.4, 1.5, 1.6, 2.1, 4.5_
|
||||
|
||||
- [x]* 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_
|
||||
|
||||
- [x]* 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 `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
|
||||
- _Requirements: 4.2, 4.3, 4.4, 4.5_
|
||||
|
||||
- [ ] 4. Final checkpoint - Build and test verification
|
||||
- [x] 4.1 Verify production build and ESLint budget
|
||||
- Run `cd frontend && npm run build` and 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_
|
||||
|
||||
- [x] 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.
|
||||
|
||||
## 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-check` with 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
|
||||
|
||||
```json
|
||||
{
|
||||
"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"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user