feat(simulation): contraband detection + dialogue confidence gate (D-075) (#425, #524)

Contraband detection (Task #425, D-065):
- New module: simulation/contraband.rs — NPC scan checks carried items
  against the KnowledgeGraph confidence gate. NPCs with Authority access
  can initiate a scan; scan outcome depends on item CarriedBy + KG entry.
- Adds ContrabandScanResult event type and ContrabanEntry component.
- Wired into simulation/mod.rs module list.

Dialogue confidence gate (Task #524, D-075 — OQ-18 resolution):
- relationship_to_trust() gains confidence parameter (KnowledgeConfidence).
- Trust tier mapping: (Friendly, KnowsDetails+)→Secret, (Friendly|Known,
  KnowsOf+)→Real, otherwise Surface. Access tier (Layer 1) unchanged.
- Caller process_talk_interaction passes observer KG confidence_of target.
- Resolves OQ-18: confidence co-gates TrustTier, not AccessTier.

Supporting changes:
- decisions/content.md: add D-075 (16 decisions, dated 2026-02-19)
- knowledge/types.rs: expose KnowledgeConfidence comparison helpers
- knowledge/registry.rs: minor API polish
- bridge/types.rs: ContrabandScanResult wire type
- bridge/text_renderer.rs: render contraband scan status
- perception/observer: include carried item count in snapshot
- npc/mod.rs: NPC scan range constant, authority flag

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 11:12:24 +01:00
co-authored by Claude Sonnet 4.6
parent 54754e4080
commit d103e445e7
12 changed files with 670 additions and 46 deletions
+98 -31
View File
@@ -171,18 +171,27 @@ pub fn available_access_tiers(relationship: RelationshipState) -> Vec<AccessTier
}
}
/// Map RelationshipState to the player's effective TrustTier.
/// Map RelationshipState + KnowledgeConfidence to the player's effective TrustTier.
///
/// v0.1 mapping:
/// - Friendly → Real (relationship depth unlocks deeper trust)
/// - All others → Surface
///
/// TODO: TrustTier::Secret is currently unreachable. It should gate on
/// KG confidence (e.g., KnowsDetails+ for a specific secret topic) rather
/// than RelationshipState alone. Tracked for Phase 2 narrative expansion.
pub fn relationship_to_trust(relationship: RelationshipState) -> TrustTier {
/// D-075 layered gate: trust requires BOTH relationship depth AND knowledge depth.
/// - Secret: Friendly + KnowsDetails+ (deep rapport + actionable knowledge)
/// - Real: (Friendly or Known) + KnowsOf+ (rapport + substantive knowledge)
/// - Surface: everything else (baseline, always available)
pub fn relationship_to_trust(
relationship: RelationshipState,
confidence: crate::knowledge::types::KnowledgeConfidence,
) -> TrustTier {
use crate::knowledge::types::KnowledgeConfidence;
match relationship {
RelationshipState::Friendly => TrustTier::Real,
RelationshipState::Friendly if confidence >= KnowledgeConfidence::KnowsDetails => {
TrustTier::Secret
}
RelationshipState::Friendly | RelationshipState::Known
if confidence >= KnowledgeConfidence::KnowsOf =>
{
TrustTier::Real
}
_ => TrustTier::Surface,
}
}
@@ -373,8 +382,11 @@ pub fn process_talk_interaction(
// Layer 2: Derive active situations from game state
let situations = derive_situations(time.day_phase(), relationship);
// Layer 3: Trust tier from relationship
let trust = relationship_to_trust(relationship);
// Layer 3: Trust tier from relationship + confidence (D-075)
let confidence = target_stable
.and_then(|sid| observer_kg.confidence_of(&sid))
.unwrap_or(crate::knowledge::types::KnowledgeConfidence::Suspects);
let trust = relationship_to_trust(relationship, confidence);
// Query Layers 1-3: collect candidates across all available access tiers
let mut candidates: Vec<&IndexedDialogueLine> = Vec::new();
@@ -565,10 +577,7 @@ const CONFRONTATION_LINES: &[(&str, &str)] = &[
"confront_01",
"That changed everything between us. No going back.",
),
(
"confront_02",
"The look on their face... they know I know.",
),
("confront_02", "The look on their face... they know I know."),
(
"confront_03",
"Cards on the table. Let's see what happens next.",
@@ -600,13 +609,8 @@ pub fn process_confrontation_response(
With<PlayerCharacter>,
>,
) {
let Ok((
player_entity,
confrontation,
mut observer_kg,
mut monologue_buf,
mut monologue_state,
)) = query.single_mut()
let Ok((player_entity, confrontation, mut observer_kg, mut monologue_buf, mut monologue_state)) =
query.single_mut()
else {
return;
};
@@ -714,26 +718,83 @@ mod tests {
assert_eq!(tiers, vec![AccessTier::Hostile]);
}
// -- Trust tier tests (D-075: layered confidence gate) --------------------
#[test]
fn trust_friendly_is_real() {
fn trust_friendly_knows_details_is_secret() {
use crate::knowledge::types::KnowledgeConfidence;
assert_eq!(
relationship_to_trust(RelationshipState::Friendly),
relationship_to_trust(
RelationshipState::Friendly,
KnowledgeConfidence::KnowsDetails
),
TrustTier::Secret
);
}
#[test]
fn trust_friendly_direct_is_secret() {
use crate::knowledge::types::KnowledgeConfidence;
assert_eq!(
relationship_to_trust(RelationshipState::Friendly, KnowledgeConfidence::Direct),
TrustTier::Secret
);
}
#[test]
fn trust_friendly_knows_of_is_real() {
use crate::knowledge::types::KnowledgeConfidence;
assert_eq!(
relationship_to_trust(RelationshipState::Friendly, KnowledgeConfidence::KnowsOf),
TrustTier::Real
);
}
#[test]
fn trust_others_are_surface() {
fn trust_friendly_suspects_is_surface() {
use crate::knowledge::types::KnowledgeConfidence;
assert_eq!(
relationship_to_trust(RelationshipState::Unknown),
relationship_to_trust(RelationshipState::Friendly, KnowledgeConfidence::Suspects),
TrustTier::Surface
);
}
#[test]
fn trust_known_knows_of_is_real() {
use crate::knowledge::types::KnowledgeConfidence;
assert_eq!(
relationship_to_trust(RelationshipState::Known),
relationship_to_trust(RelationshipState::Known, KnowledgeConfidence::KnowsOf),
TrustTier::Real
);
}
#[test]
fn trust_known_suspects_is_surface() {
use crate::knowledge::types::KnowledgeConfidence;
assert_eq!(
relationship_to_trust(RelationshipState::Known, KnowledgeConfidence::Suspects),
TrustTier::Surface
);
}
#[test]
fn trust_unknown_is_always_surface() {
use crate::knowledge::types::KnowledgeConfidence;
assert_eq!(
relationship_to_trust(RelationshipState::PersonOfInterest),
relationship_to_trust(RelationshipState::Unknown, KnowledgeConfidence::Direct),
TrustTier::Surface
);
}
#[test]
fn trust_poi_is_always_surface() {
use crate::knowledge::types::KnowledgeConfidence;
// PersonOfInterest uses Authority access, not trust depth
assert_eq!(
relationship_to_trust(
RelationshipState::PersonOfInterest,
KnowledgeConfidence::KnowsDetails
),
TrustTier::Surface
);
}
@@ -1576,7 +1637,10 @@ mod tests {
DeviationTrigger::WalkAway,
"Deviation trigger should be WalkAway"
);
assert_eq!(deviation.tick, 42, "Deviation should record the walk-away tick");
assert_eq!(
deviation.tick, 42,
"Deviation should record the walk-away tick"
);
}
#[test]
@@ -1659,7 +1723,10 @@ mod tests {
// RoutineDeviation should be recorded (symmetric with walk-away)
let deviation = world.get::<crate::npc::RoutineDeviation>(npc);
assert!(deviation.is_some(), "NPC should get RoutineDeviation after confrontation");
assert!(
deviation.is_some(),
"NPC should get RoutineDeviation after confrontation"
);
assert_eq!(
deviation.unwrap().trigger,
crate::npc::DeviationTrigger::Confrontation,