Skip to main content

canwu_sim/runtime/
knowledge.rs

1use super::records::{DomainRecordClass, DomainRecordSchemas};
2use super::{
3    CanwuError, ErrorCode, PayloadSchema, PersistentDomainRecordStore, RuntimeCurrentState,
4    SimulationSnapshot, canonical_text, component_key, is_canonical_hash,
5};
6use canwu_core::{
7    CoreEntityKind, DomainRecordKind, EntityRef, KnowledgeHolderPolicy, KnowledgeHolderRef,
8    KnowledgeRecordKind, KnowledgeSchemaId,
9};
10use canwu_knowledge::{
11    DEFAULT_KNOWLEDGE_PAGE_SIZE, KnowledgeRecord, KnowledgeRecordDraft, KnowledgeSubject,
12    KnowledgeSubjectTarget, MAX_KNOWLEDGE_PAGE_SIZE,
13};
14use serde::{Deserialize, Serialize};
15use std::collections::BTreeMap;
16
17#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
18pub struct KnowledgeLimitsV1 {
19    pub schemas_per_plugin: usize,
20    pub records_per_batch: usize,
21    pub batches_per_system_boundary: usize,
22    pub records_per_boundary: usize,
23    pub payload_bytes_per_record: usize,
24    pub relations_per_record: usize,
25    pub text_bytes: usize,
26    pub max_page_size: u32,
27    pub queries_per_batch: usize,
28    pub ids_per_direct_get: usize,
29    pub default_page_size: u32,
30    pub relation_trace_depth: u32,
31    pub relation_graph_records: usize,
32}
33
34impl KnowledgeLimitsV1 {
35    pub const CURRENT: Self = Self {
36        schemas_per_plugin: 256,
37        records_per_batch: 1_000,
38        batches_per_system_boundary: 64,
39        records_per_boundary: 10_000,
40        payload_bytes_per_record: 65_536,
41        relations_per_record: 64,
42        text_bytes: 1_024,
43        max_page_size: MAX_KNOWLEDGE_PAGE_SIZE,
44        queries_per_batch: 64,
45        ids_per_direct_get: 1_000,
46        default_page_size: DEFAULT_KNOWLEDGE_PAGE_SIZE,
47        relation_trace_depth: 32,
48        relation_graph_records: 10_000,
49    };
50}
51
52#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
53#[serde(tag = "type", content = "kind", rename_all = "snake_case")]
54pub enum KnowledgeSubjectTargetKind {
55    Core(CoreEntityKind),
56    Domain(DomainRecordKind),
57    AnyEntity,
58    Event,
59}
60
61#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
62pub struct KnowledgeSubjectSchema {
63    pub role: String,
64    pub targets: Vec<KnowledgeSubjectTargetKind>,
65    pub required: bool,
66    pub multiple: bool,
67}
68
69#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
70pub struct PluginKnowledgeSchema {
71    pub id: KnowledgeSchemaId,
72    pub schema_hash: String,
73    pub writable: bool,
74    pub payload_schema: PayloadSchema,
75    pub subjects: Vec<KnowledgeSubjectSchema>,
76}
77
78impl PluginKnowledgeSchema {
79    pub(crate) fn canonicalize(&mut self) {
80        for subject in &mut self.subjects {
81            subject.targets.sort();
82            subject.targets.dedup();
83        }
84        self.subjects
85            .sort_by(|left, right| left.role.cmp(&right.role));
86    }
87
88    pub(crate) fn validate(&self) -> Result<(), CanwuError> {
89        validate_kind(&self.id.kind)?;
90        if self.id.version == 0 {
91            return Err(invalid_schema("knowledge schema versions must be nonzero"));
92        }
93        if !is_canonical_hash(&self.schema_hash) {
94            return Err(invalid_schema(
95                "knowledge schemas require a canonical semantic hash",
96            ));
97        }
98        if self
99            .subjects
100            .windows(2)
101            .any(|pair| pair[0].role >= pair[1].role)
102        {
103            return Err(invalid_schema(
104                "knowledge subject roles must be unique and sorted",
105            ));
106        }
107        for subject in &self.subjects {
108            if !canonical_text(&subject.role)
109                || subject.targets.is_empty()
110                || subject.targets.windows(2).any(|pair| pair[0] >= pair[1])
111            {
112                return Err(invalid_schema(
113                    "knowledge subjects require canonical roles and unique sorted targets",
114                ));
115            }
116            for target in &subject.targets {
117                if let KnowledgeSubjectTargetKind::Domain(kind) = target {
118                    validate_domain_kind(kind)?;
119                }
120            }
121        }
122        Ok(())
123    }
124}
125
126pub(crate) type KnowledgeSchemas = BTreeMap<KnowledgeSchemaId, (String, PluginKnowledgeSchema)>;
127pub(crate) type KnowledgeKindOwners = BTreeMap<KnowledgeRecordKind, String>;
128
129pub(crate) fn validate_schema_set(
130    schemas: &KnowledgeSchemas,
131    owners: &KnowledgeKindOwners,
132) -> Result<(), CanwuError> {
133    let mut writable = BTreeMap::<KnowledgeRecordKind, usize>::new();
134    for (id, (owner, schema)) in schemas {
135        if id != &schema.id || owners.get(&id.kind) != Some(owner) {
136            return Err(invalid_schema(
137                "knowledge schema registry ownership is inconsistent",
138            ));
139        }
140        schema.validate()?;
141        if schema.writable {
142            *writable.entry(id.kind.clone()).or_default() += 1;
143        }
144    }
145    if owners.keys().any(|kind| {
146        !schemas.keys().any(|schema| &schema.kind == kind)
147            || writable.get(kind).copied().unwrap_or_default() != 1
148    }) {
149        return Err(invalid_schema(
150            "each knowledge kind requires exactly one writable version",
151        ));
152    }
153    Ok(())
154}
155
156pub(crate) fn validate_draft(
157    draft: &KnowledgeRecordDraft,
158    schema: &PluginKnowledgeSchema,
159    holder: &KnowledgeHolderRef,
160    current: &RuntimeCurrentState,
161    record_schemas: &DomainRecordSchemas,
162) -> Result<(), CanwuError> {
163    validate_record_shape(draft, schema, holder, current, record_schemas, true)
164}
165
166fn validate_record_shape(
167    draft: &KnowledgeRecordDraft,
168    schema: &PluginKnowledgeSchema,
169    holder: &KnowledgeHolderRef,
170    current: &RuntimeCurrentState,
171    record_schemas: &DomainRecordSchemas,
172    require_writable: bool,
173) -> Result<(), CanwuError> {
174    if draft.schema != schema.id {
175        return Err(invalid_knowledge(
176            "knowledge draft names the wrong schema version",
177        ));
178    }
179    if require_writable && !schema.writable {
180        return Err(invalid_knowledge(
181            "knowledge draft uses a read-only schema version",
182        ));
183    }
184    if require_writable {
185        validate_holder_for_publication(holder, current, record_schemas)?;
186    } else {
187        validate_historical_holder(holder, current, record_schemas)?;
188    }
189    if draft.confidence_per_mille > 1_000 {
190        return Err(invalid_knowledge(
191            "knowledge confidence exceeds 1000 per mille",
192        ));
193    }
194    if !canonical_text(&draft.origin.method)
195        || draft.origin.method.len() > KnowledgeLimitsV1::CURRENT.text_bytes
196    {
197        return Err(invalid_knowledge(
198            "knowledge origin method is not canonical or exceeds its limit",
199        ));
200    }
201    if draft.subjects.len() > KnowledgeLimitsV1::CURRENT.relations_per_record
202        || draft.origin.evidence.len() > KnowledgeLimitsV1::CURRENT.relations_per_record
203        || draft.supersedes.len() > KnowledgeLimitsV1::CURRENT.relations_per_record
204        || draft.contradicts.len() > KnowledgeLimitsV1::CURRENT.relations_per_record
205    {
206        return Err(invalid_knowledge(
207            "knowledge record relation limit exceeded",
208        ));
209    }
210    if !strictly_sorted(&draft.subjects) || !strictly_sorted(&draft.origin.evidence) {
211        return Err(invalid_knowledge(
212            "knowledge subjects and evidence references must be sorted and unique",
213        ));
214    }
215    let payload_bytes = serde_json::to_vec(&draft.payload).map_err(|error| {
216        invalid_knowledge(format!("knowledge payload encoding failed: {error}"))
217    })?;
218    if payload_bytes.len() > KnowledgeLimitsV1::CURRENT.payload_bytes_per_record {
219        return Err(invalid_knowledge("knowledge payload byte limit exceeded"));
220    }
221    schema.payload_schema.validate(&draft.payload)?;
222    validate_subjects(&draft.subjects, schema, current, record_schemas)?;
223    validate_relations(draft)?;
224    Ok(())
225}
226
227pub(crate) fn validate_stored_record(
228    record: &KnowledgeRecord,
229    schemas: &KnowledgeSchemas,
230    current: &RuntimeCurrentState,
231    record_schemas: &DomainRecordSchemas,
232) -> Result<(), CanwuError> {
233    let Some((_, schema)) = schemas.get(&record.schema) else {
234        return Err(invalid_knowledge(
235            "knowledge record uses an unregistered schema",
236        ));
237    };
238    let draft = KnowledgeRecordDraft {
239        schema: record.schema.clone(),
240        subjects: record.subjects.clone(),
241        payload: record.payload.clone(),
242        as_of: record.as_of,
243        confidence_per_mille: record.confidence_per_mille,
244        origin: record.origin.clone(),
245        supersedes: record.supersedes.clone(),
246        contradicts: record.contradicts.clone(),
247    };
248    validate_record_shape(
249        &draft,
250        schema,
251        &record.holder,
252        current,
253        record_schemas,
254        false,
255    )
256}
257
258pub(crate) fn validate_snapshot_records(
259    snapshot: &SimulationSnapshot,
260    schemas: &KnowledgeSchemas,
261    record_schemas: &DomainRecordSchemas,
262) -> Result<u64, CanwuError> {
263    let current = RuntimeCurrentState {
264        entities: snapshot.entities.iter().cloned().collect(),
265        people: snapshot
266            .world
267            .people
268            .iter()
269            .cloned()
270            .map(|value| (value.id, value))
271            .collect(),
272        letters: snapshot
273            .world
274            .letters
275            .iter()
276            .cloned()
277            .map(|value| (value.id, value))
278            .collect(),
279        governments: snapshot
280            .world
281            .governments
282            .iter()
283            .cloned()
284            .map(|value| (value.id, value))
285            .collect(),
286        territories: snapshot
287            .world
288            .territories
289            .iter()
290            .cloned()
291            .map(|value| (value.id, value))
292            .collect(),
293        routes: snapshot
294            .world
295            .routes
296            .iter()
297            .cloned()
298            .map(|value| (value.id, value))
299            .collect(),
300        armies: snapshot
301            .world
302            .armies
303            .iter()
304            .cloned()
305            .map(|value| (value.id, value))
306            .collect(),
307        knowledge: snapshot.knowledge.clone(),
308        plugin_components: snapshot
309            .plugin_components
310            .iter()
311            .cloned()
312            .map(|record| {
313                (
314                    component_key(
315                        &record.plugin,
316                        &record.state,
317                        &record.entity,
318                        &record.component,
319                    ),
320                    record,
321                )
322            })
323            .collect(),
324        domain_records: PersistentDomainRecordStore::from_records(
325            snapshot
326                .domain_records
327                .iter()
328                .cloned()
329                .map(|record| (record.reference.clone(), record))
330                .collect(),
331        )?,
332        decisions: snapshot.decisions.clone(),
333        root_seed: snapshot.root_seed,
334        authority_root_seed: snapshot.authority_root_seed,
335        random_streams: snapshot
336            .random_streams
337            .iter()
338            .cloned()
339            .map(|stream| (stream.key.clone(), stream))
340            .collect(),
341    };
342    let mut global_ids = std::collections::BTreeSet::new();
343    let mut max_id = 0_u64;
344    for (holder, records) in &snapshot.knowledge.records {
345        for (id, record) in records {
346            if *id != record.id || &record.holder != holder || !global_ids.insert(*id) {
347                return Err(invalid_knowledge(
348                    "knowledge ledger keys, holders, and global IDs must be consistent and unique",
349                ));
350            }
351            if record.learned_at > snapshot.now {
352                return Err(invalid_knowledge(
353                    "knowledge records cannot be learned after the snapshot time",
354                ));
355            }
356            if record
357                .supersedes
358                .iter()
359                .chain(&record.contradicts)
360                .any(|related| !records.contains_key(related))
361            {
362                return Err(invalid_knowledge(
363                    "knowledge relations must resolve within the same holder ledger",
364                ));
365            }
366            validate_stored_record(record, schemas, &current, record_schemas)?;
367            max_id = max_id.max(id.get());
368        }
369    }
370    Ok(max_id)
371}
372
373pub(crate) fn validate_holder_for_publication(
374    holder: &KnowledgeHolderRef,
375    current: &RuntimeCurrentState,
376    record_schemas: &DomainRecordSchemas,
377) -> Result<(), CanwuError> {
378    let allowed = match holder {
379        KnowledgeHolderRef::Person(id) => {
380            current.entities.contains(&EntityRef::Person(*id)) || current.people.contains_key(id)
381        }
382        KnowledgeHolderRef::Entity(EntityRef::Army(id)) => {
383            current.entities.contains(&EntityRef::Army(*id)) || current.armies.contains_key(id)
384        }
385        KnowledgeHolderRef::Entity(EntityRef::Government(id)) => {
386            current.entities.contains(&EntityRef::Government(*id))
387                || current.governments.contains_key(id)
388        }
389        KnowledgeHolderRef::Entity(
390            EntityRef::Organization(_)
391            | EntityRef::Person(_)
392            | EntityRef::Resource(_)
393            | EntityRef::Route(_)
394            | EntityRef::Territory(_),
395        ) => false,
396        KnowledgeHolderRef::Entity(EntityRef::Domain(reference)) => {
397            current.domain_records.get(reference).is_some_and(|record| {
398                !record.is_deleted()
399                    && record.is_active()
400                    && record.class == DomainRecordClass::Entity
401                    && record_schemas
402                        .get(&reference.kind)
403                        .is_some_and(|(_, schema)| {
404                            schema.holder_policy == KnowledgeHolderPolicy::Allowed
405                        })
406            })
407        }
408    };
409    if !allowed {
410        return Err(CanwuError::new(
411            ErrorCode::InvalidKnowledgeHolder,
412            "knowledge holder is missing, retired, deleted, duplicated as a person entity, or ineligible",
413        ));
414    }
415    Ok(())
416}
417
418fn validate_historical_holder(
419    holder: &KnowledgeHolderRef,
420    current: &RuntimeCurrentState,
421    record_schemas: &DomainRecordSchemas,
422) -> Result<(), CanwuError> {
423    let allowed = match holder {
424        KnowledgeHolderRef::Person(id) => {
425            current.entities.contains(&EntityRef::Person(*id)) || current.people.contains_key(id)
426        }
427        KnowledgeHolderRef::Entity(EntityRef::Army(id)) => {
428            current.entities.contains(&EntityRef::Army(*id)) || current.armies.contains_key(id)
429        }
430        KnowledgeHolderRef::Entity(EntityRef::Government(id)) => {
431            current.entities.contains(&EntityRef::Government(*id))
432                || current.governments.contains_key(id)
433        }
434        KnowledgeHolderRef::Entity(
435            EntityRef::Organization(_)
436            | EntityRef::Person(_)
437            | EntityRef::Resource(_)
438            | EntityRef::Route(_)
439            | EntityRef::Territory(_),
440        ) => false,
441        KnowledgeHolderRef::Entity(EntityRef::Domain(reference)) => {
442            current.domain_records.get(reference).is_some_and(|record| {
443                record.class == DomainRecordClass::Entity
444                    && record_schemas
445                        .get(&reference.kind)
446                        .is_some_and(|(_, schema)| {
447                            schema.holder_policy == KnowledgeHolderPolicy::Allowed
448                        })
449            })
450        }
451    };
452    if !allowed {
453        return Err(CanwuError::new(
454            ErrorCode::InvalidKnowledgeHolder,
455            "knowledge record names an unknown or ineligible historical holder",
456        ));
457    }
458    Ok(())
459}
460
461fn validate_subjects(
462    subjects: &[KnowledgeSubject],
463    schema: &PluginKnowledgeSchema,
464    current: &RuntimeCurrentState,
465    record_schemas: &DomainRecordSchemas,
466) -> Result<(), CanwuError> {
467    let mut counts = BTreeMap::<&str, usize>::new();
468    for subject in subjects {
469        if !canonical_text(&subject.role) {
470            return Err(invalid_knowledge(
471                "knowledge subject roles must be canonical",
472            ));
473        }
474        let Some(declaration) = schema
475            .subjects
476            .iter()
477            .find(|item| item.role == subject.role)
478        else {
479            return Err(invalid_knowledge(
480                "knowledge record contains an undeclared subject role",
481            ));
482        };
483        if !subject_matches(subject, declaration, current, record_schemas) {
484            return Err(invalid_knowledge(
485                "knowledge subject target does not match its schema role",
486            ));
487        }
488        let count = counts.entry(&subject.role).or_default();
489        *count += 1;
490        if !declaration.multiple && *count > 1 {
491            return Err(invalid_knowledge(
492                "knowledge singleton subject role is repeated",
493            ));
494        }
495    }
496    if schema.subjects.iter().any(|declaration| {
497        declaration.required
498            && counts
499                .get(declaration.role.as_str())
500                .copied()
501                .unwrap_or_default()
502                == 0
503    }) {
504        return Err(invalid_knowledge(
505            "knowledge record is missing a required subject role",
506        ));
507    }
508    Ok(())
509}
510
511fn subject_matches(
512    subject: &KnowledgeSubject,
513    schema: &KnowledgeSubjectSchema,
514    current: &RuntimeCurrentState,
515    _record_schemas: &DomainRecordSchemas,
516) -> bool {
517    match &subject.target {
518        KnowledgeSubjectTarget::Event(_) => {
519            schema.targets.contains(&KnowledgeSubjectTargetKind::Event)
520        }
521        KnowledgeSubjectTarget::Entity(entity) => {
522            let exact = entity.core_kind().is_some_and(|kind| {
523                schema
524                    .targets
525                    .contains(&KnowledgeSubjectTargetKind::Core(kind))
526            }) && entity_exists(current, entity);
527            let any = schema
528                .targets
529                .contains(&KnowledgeSubjectTargetKind::AnyEntity)
530                && entity_exists(current, entity);
531            exact || any
532        }
533        KnowledgeSubjectTarget::DomainRecord(reference) => {
534            let exact = schema
535                .targets
536                .contains(&KnowledgeSubjectTargetKind::Domain(reference.kind.clone()));
537            let any = schema
538                .targets
539                .contains(&KnowledgeSubjectTargetKind::AnyEntity)
540                && current.domain_records.get(reference).is_some_and(|record| {
541                    !record.is_deleted() && record.class == DomainRecordClass::Entity
542                });
543            (exact || any) && current.domain_records.contains_key(reference)
544        }
545    }
546}
547
548fn entity_exists(current: &RuntimeCurrentState, entity: &EntityRef) -> bool {
549    match entity {
550        EntityRef::Army(id) => current.entities.contains(entity) || current.armies.contains_key(id),
551        EntityRef::Government(id) => {
552            current.entities.contains(entity) || current.governments.contains_key(id)
553        }
554        EntityRef::Organization(_) | EntityRef::Resource(_) => current.entities.contains(entity),
555        EntityRef::Person(id) => {
556            current.entities.contains(entity) || current.people.contains_key(id)
557        }
558        EntityRef::Domain(reference) => {
559            current.domain_records.get(reference).is_some_and(|record| {
560                !record.is_deleted() && record.class == DomainRecordClass::Entity
561            })
562        }
563        EntityRef::Route(id) => {
564            current.entities.contains(entity) || current.routes.contains_key(id)
565        }
566        EntityRef::Territory(id) => {
567            current.entities.contains(entity) || current.territories.contains_key(id)
568        }
569    }
570}
571
572fn validate_relations(draft: &KnowledgeRecordDraft) -> Result<(), CanwuError> {
573    if !strictly_sorted(&draft.supersedes)
574        || !strictly_sorted(&draft.contradicts)
575        || draft
576            .supersedes
577            .iter()
578            .any(|id| draft.contradicts.contains(id))
579    {
580        return Err(invalid_knowledge(
581            "knowledge supersedes and contradicts relations must be sorted, unique, and disjoint",
582        ));
583    }
584    Ok(())
585}
586
587fn strictly_sorted<T: Ord>(values: &[T]) -> bool {
588    values.windows(2).all(|pair| pair[0] < pair[1])
589}
590
591fn validate_kind(kind: &KnowledgeRecordKind) -> Result<(), CanwuError> {
592    if !canonical_text(&kind.namespace) || !canonical_text(&kind.name) {
593        return Err(invalid_schema("knowledge schema kind must be canonical"));
594    }
595    Ok(())
596}
597
598fn validate_domain_kind(kind: &DomainRecordKind) -> Result<(), CanwuError> {
599    if !canonical_text(&kind.namespace) || !canonical_text(&kind.name) {
600        return Err(invalid_schema(
601            "knowledge subject domain kind must be canonical",
602        ));
603    }
604    Ok(())
605}
606
607fn invalid_schema(message: impl Into<String>) -> CanwuError {
608    CanwuError::new(ErrorCode::InvalidKnowledgeSchema, message)
609}
610
611fn invalid_knowledge(message: impl Into<String>) -> CanwuError {
612    CanwuError::new(ErrorCode::InvalidKnowledgeRecord, message)
613}
614
615#[cfg(test)]
616mod tests {
617    use super::super::records::{DomainRecord, DomainRecordLifecycle, DomainRecordSchema};
618    use super::*;
619    use crate::DecisionState;
620    use crate::runtime::{Government, MapPoint, Route, Territory};
621    use canwu_core::{
622        DomainRecordRef, EventId, EvidenceRef, GovernmentId, KnowledgeRecordId, PersonId,
623        ResourceId, RouteId, TerritoryId,
624    };
625    use canwu_knowledge::{KnowledgeOrigin, KnowledgeSnapshot};
626    use canwu_time::SimTime;
627    use serde_json::json;
628    use std::collections::{BTreeMap, BTreeSet};
629
630    fn knowledge_kind() -> KnowledgeRecordKind {
631        KnowledgeRecordKind::new("fixture.knowledge", "assessment")
632    }
633
634    fn knowledge_schema(subjects: Vec<KnowledgeSubjectSchema>) -> PluginKnowledgeSchema {
635        PluginKnowledgeSchema {
636            id: KnowledgeSchemaId::new(knowledge_kind(), 1),
637            schema_hash: "1000000000000000000000000000000000000000000000000000000000000000"
638                .to_owned(),
639            writable: true,
640            payload_schema: PayloadSchema::Any,
641            subjects,
642        }
643    }
644
645    fn current_state(domain_records: Vec<DomainRecord>) -> RuntimeCurrentState {
646        RuntimeCurrentState {
647            entities: BTreeSet::new(),
648            people: BTreeMap::new(),
649            letters: BTreeMap::new(),
650            governments: BTreeMap::new(),
651            territories: BTreeMap::new(),
652            routes: BTreeMap::new(),
653            armies: BTreeMap::new(),
654            knowledge: KnowledgeSnapshot::default(),
655            plugin_components: BTreeMap::new(),
656            domain_records: PersistentDomainRecordStore::from_records(
657                domain_records
658                    .into_iter()
659                    .map(|record| (record.reference.clone(), record))
660                    .collect(),
661            )
662            .unwrap(),
663            decisions: DecisionState::default(),
664            root_seed: 1,
665            authority_root_seed: 1,
666            random_streams: BTreeMap::new(),
667        }
668    }
669
670    fn domain_record(
671        reference: DomainRecordRef,
672        class: DomainRecordClass,
673        lifecycle: DomainRecordLifecycle,
674    ) -> DomainRecord {
675        DomainRecord {
676            reference,
677            owner: "fixture-domain".to_owned(),
678            class,
679            version: 1,
680            lifecycle,
681            payload: json!(null),
682            references: vec![],
683        }
684    }
685
686    fn holder_schemas(
687        kind: &DomainRecordKind,
688        policy: KnowledgeHolderPolicy,
689    ) -> DomainRecordSchemas {
690        let mut schema = DomainRecordSchema::new(kind.clone(), DomainRecordClass::Entity);
691        schema.holder_policy = policy;
692        BTreeMap::from([(kind.clone(), ("fixture-domain".to_owned(), schema))])
693    }
694
695    fn base_draft() -> KnowledgeRecordDraft {
696        KnowledgeRecordDraft {
697            schema: KnowledgeSchemaId::new(knowledge_kind(), 1),
698            subjects: vec![],
699            payload: json!(null),
700            as_of: None,
701            confidence_per_mille: 1_000,
702            origin: KnowledgeOrigin {
703                method: "fixture".to_owned(),
704                evidence: vec![],
705            },
706            supersedes: vec![],
707            contradicts: vec![],
708        }
709    }
710
711    fn government_current() -> (KnowledgeHolderRef, RuntimeCurrentState) {
712        let government = Government {
713            id: GovernmentId::new(1),
714            name: "Fixture".to_owned(),
715            capital: TerritoryId::new(1),
716        };
717        let mut current = current_state(vec![]);
718        current.governments.insert(government.id, government);
719        (
720            KnowledgeHolderRef::Entity(EntityRef::Government(GovernmentId::new(1))),
721            current,
722        )
723    }
724
725    #[test]
726    fn holder_eligibility_distinguishes_publication_from_retained_history() {
727        let kind = DomainRecordKind::new("fixture.organization", "office");
728        let active_ref = DomainRecordRef::new(&kind.namespace, &kind.name, "active");
729        let retired_ref = DomainRecordRef::new(&kind.namespace, &kind.name, "retired");
730        let deleted_ref = DomainRecordRef::new(&kind.namespace, &kind.name, "deleted");
731        let current = current_state(vec![
732            domain_record(
733                active_ref.clone(),
734                DomainRecordClass::Entity,
735                DomainRecordLifecycle::Active,
736            ),
737            domain_record(
738                retired_ref.clone(),
739                DomainRecordClass::Entity,
740                DomainRecordLifecycle::Retired {
741                    at: SimTime::EPOCH,
742                    successor: Some(active_ref.clone()),
743                },
744            ),
745            domain_record(
746                deleted_ref.clone(),
747                DomainRecordClass::Entity,
748                DomainRecordLifecycle::Deleted { at: SimTime::EPOCH },
749            ),
750        ]);
751        let schemas = holder_schemas(&kind, KnowledgeHolderPolicy::Allowed);
752        let active = KnowledgeHolderRef::Entity(EntityRef::Domain(active_ref));
753        let retired = KnowledgeHolderRef::Entity(EntityRef::Domain(retired_ref));
754        let deleted = KnowledgeHolderRef::Entity(EntityRef::Domain(deleted_ref));
755
756        assert!(validate_holder_for_publication(&active, &current, &schemas).is_ok());
757        assert!(validate_holder_for_publication(&retired, &current, &schemas).is_err());
758        assert!(validate_holder_for_publication(&deleted, &current, &schemas).is_err());
759        assert!(validate_historical_holder(&retired, &current, &schemas).is_ok());
760        assert!(validate_historical_holder(&deleted, &current, &schemas).is_ok());
761
762        let disallowed = holder_schemas(&kind, KnowledgeHolderPolicy::Disallowed);
763        assert!(validate_holder_for_publication(&active, &current, &disallowed).is_err());
764        for holder in [
765            KnowledgeHolderRef::Entity(EntityRef::Person(PersonId::new(1))),
766            KnowledgeHolderRef::Entity(EntityRef::Resource(ResourceId::new(1))),
767            KnowledgeHolderRef::Entity(EntityRef::Route(RouteId::new(1))),
768            KnowledgeHolderRef::Entity(EntityRef::Territory(TerritoryId::new(1))),
769        ] {
770            let error = validate_holder_for_publication(&holder, &current, &schemas)
771                .expect_err("ineligible core holder aliases must be rejected");
772            assert_eq!(error.code, ErrorCode::InvalidKnowledgeHolder);
773        }
774    }
775
776    #[test]
777    fn generic_person_registry_is_an_eligible_person_holder() {
778        let person = PersonId::new(7);
779        let holder = KnowledgeHolderRef::Person(person);
780        let mut current = current_state(vec![]);
781        current.entities.insert(EntityRef::Person(person));
782
783        assert!(validate_holder_for_publication(&holder, &current, &BTreeMap::new()).is_ok());
784        assert!(validate_historical_holder(&holder, &current, &BTreeMap::new()).is_ok());
785    }
786
787    #[test]
788    fn any_entity_accepts_future_entity_kinds_but_rejects_value_records() {
789        let entity_kind = DomainRecordKind::new("future.organization", "institution");
790        let value_kind = DomainRecordKind::new("future.organization", "memo");
791        let entity_ref = DomainRecordRef::new(&entity_kind.namespace, &entity_kind.name, "one");
792        let value_ref = DomainRecordRef::new(&value_kind.namespace, &value_kind.name, "one");
793        let mut current = current_state(vec![
794            domain_record(
795                entity_ref.clone(),
796                DomainRecordClass::Entity,
797                DomainRecordLifecycle::Active,
798            ),
799            domain_record(
800                value_ref.clone(),
801                DomainRecordClass::Record,
802                DomainRecordLifecycle::Active,
803            ),
804        ]);
805        current.territories.insert(
806            TerritoryId::new(1),
807            Territory {
808                id: TerritoryId::new(1),
809                name: "Territory".to_owned(),
810                controller: GovernmentId::new(1),
811                position: MapPoint::default(),
812            },
813        );
814        current.routes.insert(
815            RouteId::new(1),
816            Route {
817                id: RouteId::new(1),
818                name: "Route".to_owned(),
819                from: TerritoryId::new(1),
820                to: TerritoryId::new(2),
821                travel_minutes: 1,
822                terrain: "road".to_owned(),
823            },
824        );
825        let schemas = holder_schemas(&entity_kind, KnowledgeHolderPolicy::Allowed);
826        let schema = knowledge_schema(vec![KnowledgeSubjectSchema {
827            role: "subject".to_owned(),
828            targets: vec![KnowledgeSubjectTargetKind::AnyEntity],
829            required: true,
830            multiple: false,
831        }]);
832        let holder = KnowledgeHolderRef::Entity(EntityRef::Domain(entity_ref.clone()));
833        let mut draft = base_draft();
834        draft.subjects = vec![KnowledgeSubject {
835            role: "subject".to_owned(),
836            target: KnowledgeSubjectTarget::Entity(EntityRef::Domain(entity_ref)),
837        }];
838        assert!(validate_draft(&draft, &schema, &holder, &current, &schemas).is_ok());
839
840        draft.subjects[0].target =
841            KnowledgeSubjectTarget::Entity(EntityRef::Territory(TerritoryId::new(1)));
842        assert!(validate_draft(&draft, &schema, &holder, &current, &schemas).is_ok());
843        draft.subjects[0].target =
844            KnowledgeSubjectTarget::Entity(EntityRef::Route(RouteId::new(1)));
845        assert!(validate_draft(&draft, &schema, &holder, &current, &schemas).is_ok());
846
847        draft.subjects[0].target = KnowledgeSubjectTarget::DomainRecord(value_ref);
848        let error = validate_draft(&draft, &schema, &holder, &current, &schemas)
849            .expect_err("AnyEntity must not accept ordinary domain value records");
850        assert_eq!(error.code, ErrorCode::InvalidKnowledgeRecord);
851
852        let exact_person = knowledge_schema(vec![KnowledgeSubjectSchema {
853            role: "subject".to_owned(),
854            targets: vec![KnowledgeSubjectTargetKind::Core(CoreEntityKind::Person)],
855            required: true,
856            multiple: false,
857        }]);
858        draft.subjects[0].target =
859            KnowledgeSubjectTarget::Entity(EntityRef::Person(PersonId::new(999)));
860        let error = validate_draft(&draft, &exact_person, &holder, &current, &schemas)
861            .expect_err("an exact core target must still name an existing entity");
862        assert_eq!(error.code, ErrorCode::InvalidKnowledgeRecord);
863    }
864
865    #[test]
866    fn schema_set_requires_exact_ownership_and_one_writable_version() {
867        let writable = knowledge_schema(vec![]);
868        let read_only = PluginKnowledgeSchema {
869            id: KnowledgeSchemaId::new(knowledge_kind(), 2),
870            writable: false,
871            ..writable.clone()
872        };
873        let owners = BTreeMap::from([(knowledge_kind(), "fixture".to_owned())]);
874        let schemas = BTreeMap::from([
875            (
876                writable.id.clone(),
877                ("fixture".to_owned(), writable.clone()),
878            ),
879            (
880                read_only.id.clone(),
881                ("fixture".to_owned(), read_only.clone()),
882            ),
883        ]);
884        assert!(validate_schema_set(&schemas, &owners).is_ok());
885
886        let only_read_only =
887            BTreeMap::from([(read_only.id.clone(), ("fixture".to_owned(), read_only))]);
888        assert!(validate_schema_set(&only_read_only, &owners).is_err());
889
890        let second_writable = PluginKnowledgeSchema {
891            id: KnowledgeSchemaId::new(knowledge_kind(), 2),
892            ..writable.clone()
893        };
894        let two_writable = BTreeMap::from([
895            (writable.id.clone(), ("fixture".to_owned(), writable)),
896            (
897                second_writable.id.clone(),
898                ("fixture".to_owned(), second_writable),
899            ),
900        ]);
901        assert!(validate_schema_set(&two_writable, &owners).is_err());
902    }
903
904    #[test]
905    fn per_record_limits_accept_boundary_and_reject_boundary_plus_one() {
906        let (holder, current) = government_current();
907        let schemas = BTreeMap::new();
908        let no_subjects = knowledge_schema(vec![]);
909
910        let mut payload = base_draft();
911        payload.payload =
912            json!("a".repeat(KnowledgeLimitsV1::CURRENT.payload_bytes_per_record - 2));
913        assert!(validate_draft(&payload, &no_subjects, &holder, &current, &schemas).is_ok());
914        payload.payload =
915            json!("a".repeat(KnowledgeLimitsV1::CURRENT.payload_bytes_per_record - 1));
916        assert!(validate_draft(&payload, &no_subjects, &holder, &current, &schemas).is_err());
917
918        let mut method = base_draft();
919        method.origin.method = "m".repeat(KnowledgeLimitsV1::CURRENT.text_bytes);
920        assert!(validate_draft(&method, &no_subjects, &holder, &current, &schemas).is_ok());
921        method.origin.method.push('m');
922        assert!(validate_draft(&method, &no_subjects, &holder, &current, &schemas).is_err());
923
924        let mut evidence = base_draft();
925        evidence.origin.evidence = (1..=KnowledgeLimitsV1::CURRENT.relations_per_record)
926            .map(|id| EvidenceRef::Event(EventId::new(id as u64)))
927            .collect();
928        assert!(validate_draft(&evidence, &no_subjects, &holder, &current, &schemas).is_ok());
929        evidence
930            .origin
931            .evidence
932            .push(EvidenceRef::Event(EventId::new(
933                KnowledgeLimitsV1::CURRENT.relations_per_record as u64 + 1,
934            )));
935        assert!(validate_draft(&evidence, &no_subjects, &holder, &current, &schemas).is_err());
936
937        let mut supersedes = base_draft();
938        supersedes.supersedes = (1..=KnowledgeLimitsV1::CURRENT.relations_per_record)
939            .map(|id| KnowledgeRecordId::new(id as u64))
940            .collect();
941        assert!(validate_draft(&supersedes, &no_subjects, &holder, &current, &schemas).is_ok());
942        supersedes.supersedes.push(KnowledgeRecordId::new(
943            KnowledgeLimitsV1::CURRENT.relations_per_record as u64 + 1,
944        ));
945        assert!(validate_draft(&supersedes, &no_subjects, &holder, &current, &schemas).is_err());
946
947        let mut contradicts = base_draft();
948        contradicts.contradicts = (1..=KnowledgeLimitsV1::CURRENT.relations_per_record)
949            .map(|id| KnowledgeRecordId::new(id as u64))
950            .collect();
951        assert!(validate_draft(&contradicts, &no_subjects, &holder, &current, &schemas).is_ok());
952        contradicts.contradicts.push(KnowledgeRecordId::new(
953            KnowledgeLimitsV1::CURRENT.relations_per_record as u64 + 1,
954        ));
955        assert!(validate_draft(&contradicts, &no_subjects, &holder, &current, &schemas).is_err());
956
957        let event_subject = knowledge_schema(vec![KnowledgeSubjectSchema {
958            role: "subject".to_owned(),
959            targets: vec![KnowledgeSubjectTargetKind::Event],
960            required: true,
961            multiple: true,
962        }]);
963        let mut subjects = base_draft();
964        subjects.subjects = (1..=KnowledgeLimitsV1::CURRENT.relations_per_record)
965            .map(|id| KnowledgeSubject {
966                role: "subject".to_owned(),
967                target: KnowledgeSubjectTarget::Event(EventId::new(id as u64)),
968            })
969            .collect();
970        assert!(validate_draft(&subjects, &event_subject, &holder, &current, &schemas).is_ok());
971        subjects.subjects.push(KnowledgeSubject {
972            role: "subject".to_owned(),
973            target: KnowledgeSubjectTarget::Event(EventId::new(
974                KnowledgeLimitsV1::CURRENT.relations_per_record as u64 + 1,
975            )),
976        });
977        assert!(validate_draft(&subjects, &event_subject, &holder, &current, &schemas).is_err());
978    }
979}