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, ¤t, 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, ¤t, &schemas).is_ok());
730 assert!(validate_holder_for_publication(&retired, ¤t, &schemas).is_err());
731 assert!(validate_holder_for_publication(&deleted, ¤t, &schemas).is_err());
732 assert!(validate_historical_holder(&retired, ¤t, &schemas).is_ok());
733 assert!(validate_historical_holder(&deleted, ¤t, &schemas).is_ok());
734
735 let disallowed = holder_schemas(&kind, KnowledgeHolderPolicy::Disallowed);
736 assert!(validate_holder_for_publication(&active, ¤t, &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, ¤t, &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, ¤t, &schemas).is_ok());
801
802 draft.subjects[0].target =
803 KnowledgeSubjectTarget::Entity(EntityRef::Territory(TerritoryId::new(1)));
804 assert!(validate_draft(&draft, &schema, &holder, ¤t, &schemas).is_ok());
805 draft.subjects[0].target =
806 KnowledgeSubjectTarget::Entity(EntityRef::Route(RouteId::new(1)));
807 assert!(validate_draft(&draft, &schema, &holder, ¤t, &schemas).is_ok());
808
809 draft.subjects[0].target = KnowledgeSubjectTarget::DomainRecord(value_ref);
810 let error = validate_draft(&draft, &schema, &holder, ¤t, &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, ¤t, &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, ¤t, &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, ¤t, &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, ¤t, &schemas).is_ok());
883 method.origin.method.push('m');
884 assert!(validate_draft(&method, &no_subjects, &holder, ¤t, &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, ¤t, &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, ¤t, &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, ¤t, &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, ¤t, &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, ¤t, &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, ¤t, &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, ¤t, &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, ¤t, &schemas).is_err());
940 }
941}