feat(perception): anomaly detection and recognition monologue during delay

Add AnomalyMarker component and detect_anomalies() system that flags
entities with KG relationship PersonOfInterest or Contradicted state
for urgent cognitive delay (0.3s vs 0.6s normal). Add
trigger_recognition_monologue() that fires monologue at delay START
(when grey blob appears), not at completion — the monologue IS the
recognition process per D-060. Includes v0.1 fallback recognition
lines and cooldown tracking. Fixes #450, #451.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 17:41:14 +01:00
co-authored by Claude Opus 4.6
parent ed7498cb24
commit 695d2ac843
5 changed files with 745 additions and 5 deletions
+22
View File
@@ -69,6 +69,9 @@ pub struct PendingRecognition {
pub delay_until_tick: u64,
/// What triggered this recognition.
pub trigger: RecognitionTrigger,
/// Whether a recognition monologue has been fired for this entry (#451).
/// Set by trigger_recognition_monologue to prevent re-firing each tick.
pub monologue_fired: bool,
}
/// Component: cognitive delay buffer for entity recognition (D-060).
@@ -123,6 +126,11 @@ impl CognitiveDelay {
&self.pending
}
/// Mutable access to pending recognitions (for monologue tracking, #451).
pub fn pending_mut(&mut self) -> &mut Vec<PendingRecognition> {
&mut self.pending
}
/// Number of pending recognitions.
pub fn len(&self) -> usize {
self.pending.len()
@@ -206,6 +214,7 @@ mod tests {
position: TilePosition::new(5, 5, 0),
delay_until_tick: 106,
trigger: RecognitionTrigger::Normal,
monologue_fired: false,
});
assert_eq!(delay.len(), 1);
@@ -225,6 +234,7 @@ mod tests {
position: TilePosition::new(5, 5, 0),
delay_until_tick: 106,
trigger: RecognitionTrigger::Normal,
monologue_fired: false,
});
let cancelled = delay.cancel(&StableId(1));
@@ -251,6 +261,7 @@ mod tests {
position: TilePosition::new(5, 5, 0),
delay_until_tick: 106,
trigger: RecognitionTrigger::Normal,
monologue_fired: false,
});
let ready = delay.drain_ready(105);
@@ -270,6 +281,7 @@ mod tests {
position: TilePosition::new(5, 5, 0),
delay_until_tick: 106,
trigger: RecognitionTrigger::Normal,
monologue_fired: false,
});
let ready = delay.drain_ready(106);
@@ -290,6 +302,7 @@ mod tests {
position: TilePosition::new(5, 5, 0),
delay_until_tick: 106,
trigger: RecognitionTrigger::Normal,
monologue_fired: false,
});
let ready = delay.drain_ready(200);
@@ -310,6 +323,7 @@ mod tests {
position: TilePosition::new(5, 5, 0),
delay_until_tick: 103, // Urgent: 3 ticks from tick 100
trigger: RecognitionTrigger::Urgent,
monologue_fired: false,
});
delay.push(PendingRecognition {
target: t2,
@@ -317,6 +331,7 @@ mod tests {
position: TilePosition::new(10, 10, 0),
delay_until_tick: 106, // Normal: 6 ticks from tick 100
trigger: RecognitionTrigger::Normal,
monologue_fired: false,
});
// Tick 103: only urgent should drain
@@ -345,6 +360,7 @@ mod tests {
position: TilePosition::new(5, 5, 0),
delay_until_tick: 106,
trigger: RecognitionTrigger::Normal,
monologue_fired: false,
});
assert!(delay.is_pending(&StableId(1)));
@@ -356,6 +372,7 @@ mod tests {
position: TilePosition::new(10, 10, 0),
delay_until_tick: 106,
trigger: RecognitionTrigger::Normal,
monologue_fired: false,
});
assert!(delay.is_pending(&StableId(1)));
@@ -379,6 +396,7 @@ mod tests {
position: TilePosition::new(5, 5, 0),
delay_until_tick: 106,
trigger: RecognitionTrigger::Normal,
monologue_fired: false,
});
world.spawn((KnowledgeGraph::new(), cd));
@@ -413,6 +431,7 @@ mod tests {
position: TilePosition::new(5, 5, 0),
delay_until_tick: 106,
trigger: RecognitionTrigger::Normal,
monologue_fired: false,
});
world.spawn((KnowledgeGraph::new(), cd));
@@ -449,6 +468,7 @@ mod tests {
position: TilePosition::new(5, 5, 0),
delay_until_tick: 103, // Urgent
trigger: RecognitionTrigger::Urgent,
monologue_fired: false,
});
cd.push(PendingRecognition {
target: t2,
@@ -456,6 +476,7 @@ mod tests {
position: TilePosition::new(10, 10, 0),
delay_until_tick: 106, // Normal
trigger: RecognitionTrigger::Normal,
monologue_fired: false,
});
world.spawn((KnowledgeGraph::new(), cd));
@@ -502,6 +523,7 @@ mod tests {
position: TilePosition::new(5, 5, 0),
delay_until_tick: 106,
trigger: RecognitionTrigger::Normal,
monologue_fired: false,
});
let observer = world.spawn((KnowledgeGraph::new(), cd)).id();