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