From f4d57c683688b59dfafc454f55816dceadb48dc4 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 9 Jun 2026 22:10:53 +0100 Subject: [PATCH] Label key-collision lifecycle events in the finding timeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the deferred UI half of dad1152fc: the content_replaced lifecycle event (emitted when a same-key re-detection's text is substantially different from the existing finding — a key collision) now renders as "Re-detected with different details" in the finding timeline instead of the identifier-formatter fallback, so the operator reads what actually happened in plain language. The event metadata already carries the previous and new titles. Ceremony this line deferred from the backend commit: patrol-intelligence contract Current State entry (lifecycle label map rule + the new label), ai-runtime cross-reference updated, and the label pinned in FindingsPanel.test.ts lifecycleLabels (the subsystem's accepted proof). 111 FindingsPanel tests green. --- .../v6/internal/subsystems/ai-runtime.md | 6 ++---- .../v6/internal/subsystems/patrol-intelligence.md | 12 ++++++++++++ .../components/AI/__tests__/FindingsPanel.test.ts | 10 ++++++++++ frontend-modern/src/utils/aiFindingPresentation.ts | 1 + 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index dd68e9138..c2646abb4 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -2343,10 +2343,8 @@ old issue: both `content_replaced` and `regressed` events appear). Rephrasings and identical re-detections stay event-free per the heartbeat-not-transition rule. The shared lifecycle presentation (`frontend-modern/src/utils/aiFindingPresentation.ts`, -patrol-intelligence subsystem) renders unknown lifecycle types through the -identifier-formatter fallback, so the event reads "Content replaced" today; -a dedicated label is patrol-intelligence UI work and lands with that -subsystem's own ceremony. +patrol-intelligence subsystem) labels the event "Re-detected with different +details" — see that contract's Current State entry for the UI-side rule. `TestFindingsStore_KeyCollisionRecordsContentReplacedEvent` and its companion tests (`internal/ai/findings_lifecycle_test.go`) pin the behavior. diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index 9cad14023..8870bcf3c 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -434,6 +434,18 @@ Patrol-specific presentation helpers. ## Current State +The finding lifecycle timeline renders backend lifecycle event types through +the shared label map in +`frontend-modern/src/utils/aiFindingPresentation.ts` +(`formatFindingLifecycleType`), with unknown types falling back to the +identifier formatter. The `content_replaced` event — emitted by the +ai-runtime findings store when a same-key re-detection's text is +substantially different from the existing finding (a key collision; see the +ai-runtime contract's Current State entry) — is labeled "Re-detected with +different details" so the operator timeline explains the shift in plain +language; the event's metadata carries the previous and new titles. +`FindingsPanel.test.ts` (`lifecycleLabels`) pins the label. + The Patrol findings panel (`frontend-modern/src/components/AI/FindingsPanel.tsx`) dropped its "Open related infrastructure / workloads / storage / recovery" cross-jump chip diff --git a/frontend-modern/src/components/AI/__tests__/FindingsPanel.test.ts b/frontend-modern/src/components/AI/__tests__/FindingsPanel.test.ts index b4e113da8..70672e6cd 100644 --- a/frontend-modern/src/components/AI/__tests__/FindingsPanel.test.ts +++ b/frontend-modern/src/components/AI/__tests__/FindingsPanel.test.ts @@ -1000,6 +1000,16 @@ describe('aiFindingPresentation', () => { it('has dismissed label', () => { expect(formatFindingLifecycleType('dismissed')).toBe('Dismissed'); }); + + it('labels a key-collision content replacement as a re-detection with different details', () => { + // Backend emits content_replaced when a same-key re-detection's text + // is substantially different from the existing finding (a key + // collision); the timeline label must explain the shift in operator + // language rather than the raw identifier fallback. + expect(formatFindingLifecycleType('content_replaced')).toBe( + 'Re-detected with different details', + ); + }); }); describe('formatLifecycleType', () => { diff --git a/frontend-modern/src/utils/aiFindingPresentation.ts b/frontend-modern/src/utils/aiFindingPresentation.ts index 12ed02495..8100fb1e7 100644 --- a/frontend-modern/src/utils/aiFindingPresentation.ts +++ b/frontend-modern/src/utils/aiFindingPresentation.ts @@ -109,6 +109,7 @@ const FINDING_LIFECYCLE_LABELS: Record = { loop_state: 'Loop state changed', seen_while_suppressed: 'Seen while suppressed', loop_transition_violation: 'Invalid transition blocked', + content_replaced: 'Re-detected with different details', }; const FINDING_STATUS_BADGE_CLASSES: Record = {