Skip to main content

canwu_sim/runtime/
knowledge.rs

1use super::records::{DomainRecordClass, DomainRecordSchemas};
2use super::{
3    CanwuError, ErrorCode, PayloadSchema, RuntimeCurrentState, SimulationSnapshot, canonical_text,
4    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        people: snapshot
265            .world
266            .people
267            .iter()
268            .cloned()
269            .map(|value| (value.id, value))
270            .collect(),
271        letters: snapshot
272            .world
273            .letters
274            .iter()
275            .cloned()
276            .map(|value| (value.id, value))
277            .collect(),
278        governments: snapshot
279            .world
280            .governments
281            .iter()
282            .cloned()
283            .map(|value| (value.id, value))
284            .collect(),
285        territories: snapshot
286            .world
287            .territories
288            .iter()
289            .cloned()
290            .map(|value| (value.id, value))
291            .collect(),
292        routes: snapshot
293            .world
294            .routes
295            .iter()
296            .cloned()
297            .map(|value| (value.id, value))
298            .collect(),
299        armies: snapshot
300            .world
301            .armies
302            .iter()
303            .cloned()
304            .map(|value| (value.id, value))
305            .collect(),
306        knowledge: snapshot.knowledge.clone(),
307        plugin_components: snapshot
308            .plugin_components
309            .iter()
310            .cloned()
311            .map(|record| {
312                (
313                    component_key(
314                        &record.plugin,
315                        &record.state,
316                        &record.entity,
317                        &record.component,
318                    ),
319                    record,
320                )
321            })
322            .collect(),
323        domain_records: snapshot
324            .domain_records
325            .iter()
326            .cloned()
327            .map(|record| (record.reference.clone(), record))
328            .collect(),
329        decisions: snapshot.decisions.clone(),
330        root_seed: snapshot.root_seed,
331        random_streams: snapshot
332            .random_streams
333            .iter()
334            .cloned()
335            .map(|stream| (stream.key.clone(), stream))
336            .collect(),
337    };
338    let mut global_ids = std::collections::BTreeSet::new();
339    let mut max_id = 0_u64;
340    for (holder, records) in &snapshot.knowledge.records {
341        for (id, record) in records {
342            if *id != record.id || &record.holder != holder || !global_ids.insert(*id) {
343                return Err(invalid_knowledge(
344                    "knowledge ledger keys, holders, and global IDs must be consistent and unique",
345                ));
346            }
347            if record.learned_at > snapshot.now {
348                return Err(invalid_knowledge(
349                    "knowledge records cannot be learned after the snapshot time",
350                ));
351            }
352            if record
353                .supersedes
354                .iter()
355                .chain(&record.contradicts)
356                .any(|related| !records.contains_key(related))
357            {
358                return Err(invalid_knowledge(
359                    "knowledge relations must resolve within the same holder ledger",
360                ));
361            }
362            validate_stored_record(record, schemas, &current, record_schemas)?;
363            max_id = max_id.max(id.get());
364        }
365    }
366    Ok(max_id)
367}
368
369pub(crate) fn validate_holder_for_publication(
370    holder: &KnowledgeHolderRef,
371    current: &RuntimeCurrentState,
372    record_schemas: &DomainRecordSchemas,
373) -> Result<(), CanwuError> {
374    let allowed = match holder {
375        KnowledgeHolderRef::Person(id) => current.people.contains_key(id),
376        KnowledgeHolderRef::Entity(EntityRef::Army(id)) => current.armies.contains_key(id),
377        KnowledgeHolderRef::Entity(EntityRef::Government(id)) => {
378            current.governments.contains_key(id)
379        }
380        KnowledgeHolderRef::Entity(
381            EntityRef::Organization(_)
382            | EntityRef::Person(_)
383            | EntityRef::Resource(_)
384            | EntityRef::Route(_)
385            | EntityRef::Territory(_),
386        ) => false,
387        KnowledgeHolderRef::Entity(EntityRef::Domain(reference)) => {
388            current.domain_records.get(reference).is_some_and(|record| {
389                !record.is_deleted()
390                    && record.is_active()
391                    && record.class == DomainRecordClass::Entity
392                    && record_schemas
393                        .get(&reference.kind)
394                        .is_some_and(|(_, schema)| {
395                            schema.holder_policy == KnowledgeHolderPolicy::Allowed
396                        })
397            })
398        }
399    };
400    if !allowed {
401        return Err(CanwuError::new(
402            ErrorCode::InvalidKnowledgeHolder,
403            "knowledge holder is missing, retired, deleted, duplicated as a person entity, or ineligible",
404        ));
405    }
406    Ok(())
407}
408
409fn validate_historical_holder(
410    holder: &KnowledgeHolderRef,
411    current: &RuntimeCurrentState,
412    record_schemas: &DomainRecordSchemas,
413) -> Result<(), CanwuError> {
414    let allowed = match holder {
415        KnowledgeHolderRef::Person(id) => current.people.contains_key(id),
416        KnowledgeHolderRef::Entity(EntityRef::Army(id)) => current.armies.contains_key(id),
417        KnowledgeHolderRef::Entity(EntityRef::Government(id)) => {
418            current.governments.contains_key(id)
419        }
420        KnowledgeHolderRef::Entity(
421            EntityRef::Organization(_)
422            | EntityRef::Person(_)
423            | EntityRef::Resource(_)
424            | EntityRef::Route(_)
425            | EntityRef::Territory(_),
426        ) => false,
427        KnowledgeHolderRef::Entity(EntityRef::Domain(reference)) => {
428            current.domain_records.get(reference).is_some_and(|record| {
429                record.class == DomainRecordClass::Entity
430                    && record_schemas
431                        .get(&reference.kind)
432                        .is_some_and(|(_, schema)| {
433                            schema.holder_policy == KnowledgeHolderPolicy::Allowed
434                        })
435            })
436        }
437    };
438    if !allowed {
439        return Err(CanwuError::new(
440            ErrorCode::InvalidKnowledgeHolder,
441            "knowledge record names an unknown or ineligible historical holder",
442        ));
443    }
444    Ok(())
445}
446
447fn validate_subjects(
448    subjects: &[KnowledgeSubject],
449    schema: &PluginKnowledgeSchema,
450    current: &RuntimeCurrentState,
451    record_schemas: &DomainRecordSchemas,
452) -> Result<(), CanwuError> {
453    let mut counts = BTreeMap::<&str, usize>::new();
454    for subject in subjects {
455        if !canonical_text(&subject.role) {
456            return Err(invalid_knowledge(
457                "knowledge subject roles must be canonical",
458            ));
459        }
460        let Some(declaration) = schema
461            .subjects
462            .iter()
463            .find(|item| item.role == subject.role)
464        else {
465            return Err(invalid_knowledge(
466                "knowledge record contains an undeclared subject role",
467            ));
468        };
469        if !subject_matches(subject, declaration, current, record_schemas) {
470            return Err(invalid_knowledge(
471                "knowledge subject target does not match its schema role",
472            ));
473        }
474        let count = counts.entry(&subject.role).or_default();
475        *count += 1;
476        if !declaration.multiple && *count > 1 {
477            return Err(invalid_knowledge(
478                "knowledge singleton subject role is repeated",
479            ));
480        }
481    }
482    if schema.subjects.iter().any(|declaration| {
483        declaration.required
484            && counts
485                .get(declaration.role.as_str())
486                .copied()
487                .unwrap_or_default()
488                == 0
489    }) {
490        return Err(invalid_knowledge(
491            "knowledge record is missing a required subject role",
492        ));
493    }
494    Ok(())
495}
496
497fn subject_matches(
498    subject: &KnowledgeSubject,
499    schema: &KnowledgeSubjectSchema,
500    current: &RuntimeCurrentState,
501    _record_schemas: &DomainRecordSchemas,
502) -> bool {
503    match &subject.target {
504        KnowledgeSubjectTarget::Event(_) => {
505            schema.targets.contains(&KnowledgeSubjectTargetKind::Event)
506        }
507        KnowledgeSubjectTarget::Entity(entity) => {
508            let exact = entity.core_kind().is_some_and(|kind| {
509                schema
510                    .targets
511                    .contains(&KnowledgeSubjectTargetKind::Core(kind))
512            }) && entity_exists(current, entity);
513            let any = schema
514                .targets
515                .contains(&KnowledgeSubjectTargetKind::AnyEntity)
516                && entity_exists(current, entity);
517            exact || any
518        }
519        KnowledgeSubjectTarget::DomainRecord(reference) => {
520            let exact = schema
521                .targets
522                .contains(&KnowledgeSubjectTargetKind::Domain(reference.kind.clone()));
523            let any = schema
524                .targets
525                .contains(&KnowledgeSubjectTargetKind::AnyEntity)
526                && current.domain_records.get(reference).is_some_and(|record| {
527                    !record.is_deleted() && record.class == DomainRecordClass::Entity
528                });
529            (exact || any) && current.domain_records.contains_key(reference)
530        }
531    }
532}
533
534fn entity_exists(current: &RuntimeCurrentState, entity: &EntityRef) -> bool {
535    match entity {
536        EntityRef::Army(id) => current.armies.contains_key(id),
537        EntityRef::Government(id) => current.governments.contains_key(id),
538        EntityRef::Organization(_) | EntityRef::Resource(_) => false,
539        EntityRef::Person(id) => current.people.contains_key(id),
540        EntityRef::Domain(reference) => {
541            current.domain_records.get(reference).is_some_and(|record| {
542                !record.is_deleted() && record.class == DomainRecordClass::Entity
543            })
544        }
545        EntityRef::Route(id) => current.routes.contains_key(id),
546        EntityRef::Territory(id) => current.territories.contains_key(id),
547    }
548}
549
550fn validate_relations(draft: &KnowledgeRecordDraft) -> Result<(), CanwuError> {
551    if !strictly_sorted(&draft.supersedes)
552        || !strictly_sorted(&draft.contradicts)
553        || draft
554            .supersedes
555            .iter()
556            .any(|id| draft.contradicts.contains(id))
557    {
558        return Err(invalid_knowledge(
559            "knowledge supersedes and contradicts relations must be sorted, unique, and disjoint",
560        ));
561    }
562    Ok(())
563}
564
565fn strictly_sorted<T: Ord>(values: &[T]) -> bool {
566    values.windows(2).all(|pair| pair[0] < pair[1])
567}
568
569fn validate_kind(kind: &KnowledgeRecordKind) -> Result<(), CanwuError> {
570    if !canonical_text(&kind.namespace) || !canonical_text(&kind.name) {
571        return Err(invalid_schema("knowledge schema kind must be canonical"));
572    }
573    Ok(())
574}
575
576fn validate_domain_kind(kind: &DomainRecordKind) -> Result<(), CanwuError> {
577    if !canonical_text(&kind.namespace) || !canonical_text(&kind.name) {
578        return Err(invalid_schema(
579            "knowledge subject domain kind must be canonical",
580        ));
581    }
582    Ok(())
583}
584
585fn invalid_schema(message: impl Into<String>) -> CanwuError {
586    CanwuError::new(ErrorCode::InvalidKnowledgeSchema, message)
587}
588
589fn invalid_knowledge(message: impl Into<String>) -> CanwuError {
590    CanwuError::new(ErrorCode::InvalidKnowledgeRecord, message)
591}
592
593#[cfg(test)]
594mod tests {
595    use super::super::records::{DomainRecord, DomainRecordLifecycle, DomainRecordSchema};
596    use super::*;
597    use crate::DecisionState;
598    use canwu_core::{
599        DomainRecordRef, EventId, EvidenceRef, GovernmentId, KnowledgeRecordId, PersonId,
600        ResourceId, RouteId, TerritoryId,
601    };
602    use canwu_knowledge::{KnowledgeOrigin, KnowledgeSnapshot};
603    use canwu_time::SimTime;
604    use canwu_world::{Government, MapPoint, Route, Territory};
605    use serde_json::json;
606    use std::collections::BTreeMap;
607
608    fn knowledge_kind() -> KnowledgeRecordKind {
609        KnowledgeRecordKind::new("fixture.knowledge", "assessment")
610    }
611
612    fn knowledge_schema(subjects: Vec<KnowledgeSubjectSchema>) -> PluginKnowledgeSchema {
613        PluginKnowledgeSchema {
614            id: KnowledgeSchemaId::new(knowledge_kind(), 1),
615            schema_hash: "1000000000000000000000000000000000000000000000000000000000000000"
616                .to_owned(),
617            writable: true,
618            payload_schema: PayloadSchema::Any,
619            subjects,
620        }
621    }
622
623    fn current_state(domain_records: Vec<DomainRecord>) -> RuntimeCurrentState {
624        RuntimeCurrentState {
625            people: BTreeMap::new(),
626            letters: BTreeMap::new(),
627            governments: BTreeMap::new(),
628            territories: BTreeMap::new(),
629            routes: BTreeMap::new(),
630            armies: BTreeMap::new(),
631            knowledge: KnowledgeSnapshot::default(),
632            plugin_components: BTreeMap::new(),
633            domain_records: domain_records
634                .into_iter()
635                .map(|record| (record.reference.clone(), record))
636                .collect(),
637            decisions: DecisionState::default(),
638            root_seed: 1,
639            random_streams: BTreeMap::new(),
640        }
641    }
642
643    fn domain_record(
644        reference: DomainRecordRef,
645        class: DomainRecordClass,
646        lifecycle: DomainRecordLifecycle,
647    ) -> DomainRecord {
648        DomainRecord {
649            reference,
650            owner: "fixture-domain".to_owned(),
651            class,
652            version: 1,
653            lifecycle,
654            payload: json!(null),
655            references: vec![],
656        }
657    }
658
659    fn holder_schemas(
660        kind: &DomainRecordKind,
661        policy: KnowledgeHolderPolicy,
662    ) -> DomainRecordSchemas {
663        let mut schema = DomainRecordSchema::new(kind.clone(), DomainRecordClass::Entity);
664        schema.holder_policy = policy;
665        BTreeMap::from([(kind.clone(), ("fixture-domain".to_owned(), schema))])
666    }
667
668    fn base_draft() -> KnowledgeRecordDraft {
669        KnowledgeRecordDraft {
670            schema: KnowledgeSchemaId::new(knowledge_kind(), 1),
671            subjects: vec![],
672            payload: json!(null),
673            as_of: None,
674            confidence_per_mille: 1_000,
675            origin: KnowledgeOrigin {
676                method: "fixture".to_owned(),
677                evidence: vec![],
678            },
679            supersedes: vec![],
680            contradicts: vec![],
681        }
682    }
683
684    fn government_current() -> (KnowledgeHolderRef, RuntimeCurrentState) {
685        let government = Government {
686            id: GovernmentId::new(1),
687            name: "Fixture".to_owned(),
688            capital: TerritoryId::new(1),
689        };
690        let mut current = current_state(vec![]);
691        current.governments.insert(government.id, government);
692        (
693            KnowledgeHolderRef::Entity(EntityRef::Government(GovernmentId::new(1))),
694            current,
695        )
696    }
697
698    #[test]
699    fn holder_eligibility_distinguishes_publication_from_retained_history() {
700        let kind = DomainRecordKind::new("fixture.organization", "office");
701        let active_ref = DomainRecordRef::new(&kind.namespace, &kind.name, "active");
702        let retired_ref = DomainRecordRef::new(&kind.namespace, &kind.name, "retired");
703        let deleted_ref = DomainRecordRef::new(&kind.namespace, &kind.name, "deleted");
704        let current = current_state(vec![
705            domain_record(
706                active_ref.clone(),
707                DomainRecordClass::Entity,
708                DomainRecordLifecycle::Active,
709            ),
710            domain_record(
711                retired_ref.clone(),
712                DomainRecordClass::Entity,
713                DomainRecordLifecycle::Retired {
714                    at: SimTime::EPOCH,
715                    successor: Some(active_ref.clone()),
716                },
717            ),
718            domain_record(
719                deleted_ref.clone(),
720                DomainRecordClass::Entity,
721                DomainRecordLifecycle::Deleted { at: SimTime::EPOCH },
722            ),
723        ]);
724        let schemas = holder_schemas(&kind, KnowledgeHolderPolicy::Allowed);
725        let active = KnowledgeHolderRef::Entity(EntityRef::Domain(active_ref));
726        let retired = KnowledgeHolderRef::Entity(EntityRef::Domain(retired_ref));
727        let deleted = KnowledgeHolderRef::Entity(EntityRef::Domain(deleted_ref));
728
729        assert!(validate_holder_for_publication(&active, &current, &schemas).is_ok());
730        assert!(validate_holder_for_publication(&retired, &current, &schemas).is_err());
731        assert!(validate_holder_for_publication(&deleted, &current, &schemas).is_err());
732        assert!(validate_historical_holder(&retired, &current, &schemas).is_ok());
733        assert!(validate_historical_holder(&deleted, &current, &schemas).is_ok());
734
735        let disallowed = holder_schemas(&kind, KnowledgeHolderPolicy::Disallowed);
736        assert!(validate_holder_for_publication(&active, &current, &disallowed).is_err());
737        for holder in [
738            KnowledgeHolderRef::Entity(EntityRef::Person(PersonId::new(1))),
739            KnowledgeHolderRef::Entity(EntityRef::Resource(ResourceId::new(1))),
740            KnowledgeHolderRef::Entity(EntityRef::Route(RouteId::new(1))),
741            KnowledgeHolderRef::Entity(EntityRef::Territory(TerritoryId::new(1))),
742        ] {
743            let error = validate_holder_for_publication(&holder, &current, &schemas)
744                .expect_err("ineligible core holder aliases must be rejected");
745            assert_eq!(error.code, ErrorCode::InvalidKnowledgeHolder);
746        }
747    }
748
749    #[test]
750    fn any_entity_accepts_future_entity_kinds_but_rejects_value_records() {
751        let entity_kind = DomainRecordKind::new("future.organization", "institution");
752        let value_kind = DomainRecordKind::new("future.organization", "memo");
753        let entity_ref = DomainRecordRef::new(&entity_kind.namespace, &entity_kind.name, "one");
754        let value_ref = DomainRecordRef::new(&value_kind.namespace, &value_kind.name, "one");
755        let mut current = current_state(vec![
756            domain_record(
757                entity_ref.clone(),
758                DomainRecordClass::Entity,
759                DomainRecordLifecycle::Active,
760            ),
761            domain_record(
762                value_ref.clone(),
763                DomainRecordClass::Record,
764                DomainRecordLifecycle::Active,
765            ),
766        ]);
767        current.territories.insert(
768            TerritoryId::new(1),
769            Territory {
770                id: TerritoryId::new(1),
771                name: "Territory".to_owned(),
772                controller: GovernmentId::new(1),
773                position: MapPoint::default(),
774            },
775        );
776        current.routes.insert(
777            RouteId::new(1),
778            Route {
779                id: RouteId::new(1),
780                name: "Route".to_owned(),
781                from: TerritoryId::new(1),
782                to: TerritoryId::new(2),
783                travel_minutes: 1,
784                terrain: "road".to_owned(),
785            },
786        );
787        let schemas = holder_schemas(&entity_kind, KnowledgeHolderPolicy::Allowed);
788        let schema = knowledge_schema(vec![KnowledgeSubjectSchema {
789            role: "subject".to_owned(),
790            targets: vec![KnowledgeSubjectTargetKind::AnyEntity],
791            required: true,
792            multiple: false,
793        }]);
794        let holder = KnowledgeHolderRef::Entity(EntityRef::Domain(entity_ref.clone()));
795        let mut draft = base_draft();
796        draft.subjects = vec![KnowledgeSubject {
797            role: "subject".to_owned(),
798            target: KnowledgeSubjectTarget::Entity(EntityRef::Domain(entity_ref)),
799        }];
800        assert!(validate_draft(&draft, &schema, &holder, &current, &schemas).is_ok());
801
802        draft.subjects[0].target =
803            KnowledgeSubjectTarget::Entity(EntityRef::Territory(TerritoryId::new(1)));
804        assert!(validate_draft(&draft, &schema, &holder, &current, &schemas).is_ok());
805        draft.subjects[0].target =
806            KnowledgeSubjectTarget::Entity(EntityRef::Route(RouteId::new(1)));
807        assert!(validate_draft(&draft, &schema, &holder, &current, &schemas).is_ok());
808
809        draft.subjects[0].target = KnowledgeSubjectTarget::DomainRecord(value_ref);
810        let error = validate_draft(&draft, &schema, &holder, &current, &schemas)
811            .expect_err("AnyEntity must not accept ordinary domain value records");
812        assert_eq!(error.code, ErrorCode::InvalidKnowledgeRecord);
813
814        let exact_person = knowledge_schema(vec![KnowledgeSubjectSchema {
815            role: "subject".to_owned(),
816            targets: vec![KnowledgeSubjectTargetKind::Core(CoreEntityKind::Person)],
817            required: true,
818            multiple: false,
819        }]);
820        draft.subjects[0].target =
821            KnowledgeSubjectTarget::Entity(EntityRef::Person(PersonId::new(999)));
822        let error = validate_draft(&draft, &exact_person, &holder, &current, &schemas)
823            .expect_err("an exact core target must still name an existing entity");
824        assert_eq!(error.code, ErrorCode::InvalidKnowledgeRecord);
825    }
826
827    #[test]
828    fn schema_set_requires_exact_ownership_and_one_writable_version() {
829        let writable = knowledge_schema(vec![]);
830        let read_only = PluginKnowledgeSchema {
831            id: KnowledgeSchemaId::new(knowledge_kind(), 2),
832            writable: false,
833            ..writable.clone()
834        };
835        let owners = BTreeMap::from([(knowledge_kind(), "fixture".to_owned())]);
836        let schemas = BTreeMap::from([
837            (
838                writable.id.clone(),
839                ("fixture".to_owned(), writable.clone()),
840            ),
841            (
842                read_only.id.clone(),
843                ("fixture".to_owned(), read_only.clone()),
844            ),
845        ]);
846        assert!(validate_schema_set(&schemas, &owners).is_ok());
847
848        let only_read_only =
849            BTreeMap::from([(read_only.id.clone(), ("fixture".to_owned(), read_only))]);
850        assert!(validate_schema_set(&only_read_only, &owners).is_err());
851
852        let second_writable = PluginKnowledgeSchema {
853            id: KnowledgeSchemaId::new(knowledge_kind(), 2),
854            ..writable.clone()
855        };
856        let two_writable = BTreeMap::from([
857            (writable.id.clone(), ("fixture".to_owned(), writable)),
858            (
859                second_writable.id.clone(),
860                ("fixture".to_owned(), second_writable),
861            ),
862        ]);
863        assert!(validate_schema_set(&two_writable, &owners).is_err());
864    }
865
866    #[test]
867    fn per_record_limits_accept_boundary_and_reject_boundary_plus_one() {
868        let (holder, current) = government_current();
869        let schemas = BTreeMap::new();
870        let no_subjects = knowledge_schema(vec![]);
871
872        let mut payload = base_draft();
873        payload.payload =
874            json!("a".repeat(KnowledgeLimitsV1::CURRENT.payload_bytes_per_record - 2));
875        assert!(validate_draft(&payload, &no_subjects, &holder, &current, &schemas).is_ok());
876        payload.payload =
877            json!("a".repeat(KnowledgeLimitsV1::CURRENT.payload_bytes_per_record - 1));
878        assert!(validate_draft(&payload, &no_subjects, &holder, &current, &schemas).is_err());
879
880        let mut method = base_draft();
881        method.origin.method = "m".repeat(KnowledgeLimitsV1::CURRENT.text_bytes);
882        assert!(validate_draft(&method, &no_subjects, &holder, &current, &schemas).is_ok());
883        method.origin.method.push('m');
884        assert!(validate_draft(&method, &no_subjects, &holder, &current, &schemas).is_err());
885
886        let mut evidence = base_draft();
887        evidence.origin.evidence = (1..=KnowledgeLimitsV1::CURRENT.relations_per_record)
888            .map(|id| EvidenceRef::Event(EventId::new(id as u64)))
889            .collect();
890        assert!(validate_draft(&evidence, &no_subjects, &holder, &current, &schemas).is_ok());
891        evidence
892            .origin
893            .evidence
894            .push(EvidenceRef::Event(EventId::new(
895                KnowledgeLimitsV1::CURRENT.relations_per_record as u64 + 1,
896            )));
897        assert!(validate_draft(&evidence, &no_subjects, &holder, &current, &schemas).is_err());
898
899        let mut supersedes = base_draft();
900        supersedes.supersedes = (1..=KnowledgeLimitsV1::CURRENT.relations_per_record)
901            .map(|id| KnowledgeRecordId::new(id as u64))
902            .collect();
903        assert!(validate_draft(&supersedes, &no_subjects, &holder, &current, &schemas).is_ok());
904        supersedes.supersedes.push(KnowledgeRecordId::new(
905            KnowledgeLimitsV1::CURRENT.relations_per_record as u64 + 1,
906        ));
907        assert!(validate_draft(&supersedes, &no_subjects, &holder, &current, &schemas).is_err());
908
909        let mut contradicts = base_draft();
910        contradicts.contradicts = (1..=KnowledgeLimitsV1::CURRENT.relations_per_record)
911            .map(|id| KnowledgeRecordId::new(id as u64))
912            .collect();
913        assert!(validate_draft(&contradicts, &no_subjects, &holder, &current, &schemas).is_ok());
914        contradicts.contradicts.push(KnowledgeRecordId::new(
915            KnowledgeLimitsV1::CURRENT.relations_per_record as u64 + 1,
916        ));
917        assert!(validate_draft(&contradicts, &no_subjects, &holder, &current, &schemas).is_err());
918
919        let event_subject = knowledge_schema(vec![KnowledgeSubjectSchema {
920            role: "subject".to_owned(),
921            targets: vec![KnowledgeSubjectTargetKind::Event],
922            required: true,
923            multiple: true,
924        }]);
925        let mut subjects = base_draft();
926        subjects.subjects = (1..=KnowledgeLimitsV1::CURRENT.relations_per_record)
927            .map(|id| KnowledgeSubject {
928                role: "subject".to_owned(),
929                target: KnowledgeSubjectTarget::Event(EventId::new(id as u64)),
930            })
931            .collect();
932        assert!(validate_draft(&subjects, &event_subject, &holder, &current, &schemas).is_ok());
933        subjects.subjects.push(KnowledgeSubject {
934            role: "subject".to_owned(),
935            target: KnowledgeSubjectTarget::Event(EventId::new(
936                KnowledgeLimitsV1::CURRENT.relations_per_record as u64 + 1,
937            )),
938        });
939        assert!(validate_draft(&subjects, &event_subject, &holder, &current, &schemas).is_err());
940    }
941}