Local community diffusion and institutional response
The scenario
Section titled “The scenario”A market of 1,000 people lies next to a village of 2,000. In the market, 200 people already support a new idea, and a small local network promotes it. Over two days, contact spreads awareness into the village. Between the two days, the local council chooses a public policy, which takes effect on day two. An observer then estimates how many villagers publicly align with the idea.
The question the example answers: how do you model shifting beliefs as integer headcounts that save, restore, and replay exactly, while the policy decision and the observer’s view go through Canwu’s authority and visibility checks?
The idea, cohorts, and numbers are synthetic and do not depict a real doctrine or event.
What this example shows
Section titled “What this example shows”canwu-society, an optional domain extension crate that you add next tocanwu-api. It brings its own types, record schemas, commands, and ingress, and is informally called the social diffusion simulation module.- Sparse population state: each cohort (
SocietyCohort, a population group in one territory) has an integer headcount. Each pair of a cohort and a target (AffiliationTarget, the idea or group people take a stance on) has aDispositionDistributionthat splits the headcount intoDispositionBuckets. - Diffusion:
SocialInfluenceEdgeandOrganizationNodeproduce signals,TransitionRulemoves whole people between profiles, andTransitionRemainderkeeps the fractions. - A decision ticket built by
institutional_policy_ticket, whose chosen option runs the domain commandset_institutional_policy. - Daily boundary systems, each in a fixed settlement phase, that apply the policy, settle transitions, aggregate, and materialize projections.
- An actor-relative estimate from
projection_for_viewer. - Snapshot restore with
from_society_snapshot_json,fork, and exact replay.
Run it
Section titled “Run it”cargo run -p canwu-society --example local_community_diffusionThe full output:
day 1: local diffusion: aware=118 assenting=5 public=0 hidden=2000 of 2000policy selected; population dispositions remain unchanged until the next daily boundaryday 2: institutional response applied: aware=229 assenting=23 public=5 hidden=1995 of 2000observer estimate: public=23 tied=20 (ground truth public=5 tied=0)snapshot restore, fork, and exact replay reproduced the same authoritative stateThe counts are villagers for the target new-idea. aware have heard of it,
assenting privately sympathize or more, public conform or advocate in
public, hidden have Private or Hidden visibility, and tied have a tie
to an organization.
Walkthrough
Section titled “Walkthrough”All code is in
local_community_diffusion.rs.
main reuses the entities, world, and knowledge of the deprecated
compatibility scenario Canwu::demo(42), adds one society record built by
tutorial_state(), registers SocietyPlugin, and settles four boundaries:
| Boundary | Called from | What happens |
|---|---|---|
| 1 | settle_day(&mut canwu, 1) |
Daily: day-1 diffusion |
| 2 | step_canonical() in register_policy_decision |
The controller is registered and the policy ticket opens |
| 3 | step_canonical() after drive_decision |
The trace is recorded and set_institutional_policy runs |
| 4 | settle_day(&mut canwu, 2) |
Daily: the policy applies, then day-2 diffusion |
1. Describe people as distributions
Section titled “1. Describe people as distributions”A DispositionProfile keeps seven dimensions apart. The market’s supporters
use this profile:
let source = DispositionProfile { awareness: AwarenessBand::Aware, assent: AssentBand::Sympathetic, practice: PracticeBand::Occasional, public_alignment: PublicAlignmentBand::Advocating, organizational_tie: OrganizationalTieBand::Member, mobilization: MobilizationBand::Latent, visibility: VisibilityBand::Public,};tutorial_state gives each cohort one distribution for new-idea:
insert_distribution( &mut state, "market", 1_000, vec![(neutral, 800), (source, 200)],);insert_distribution(&mut state, "village", 2_000, vec![(neutral, 2_000)]);SocietyState::validate requires the buckets of a distribution to add up to
the cohort headcount. Only the pairs you insert get a distribution, and a
transition rule creates a neutral one on demand for a cohort it affects. Each
target has its own distribution, so one cohort can hold independent stances
toward several targets.
2. Separate opportunity from conversion
Section titled “2. Separate opportunity from conversion”Two SocialInfluenceEdges point at the village: market-contact from the
market cohort and network-contact from the organization local-network.
Each adds source strength × reach × trust / 1,000,000 to the village’s
influence signal. In the market, 200 per mille of people support the idea, so
200 × 800 × 800 gives 128. The network has no links to other organizations,
so its strength is its base reach of 300 per mille, and 300 × 700 × 800
gives 168. Together the influence signal is 296 per mille.
Three TransitionRules decide who actually changes profile, starting with
awareness:
insert_rule( &mut state, "01-awareness", neutral, aware, TransitionWeights { influence: 200_000, ..TransitionWeights::default() },);settle_transitions in
solver.rs
turns that weight into 296 × 200,000 / 1,000 = 59,200 per million.
2,000 × 59,200 / 1,000,000 = 118.4, so 118 people become aware on day 1, and
the 0.4 person is stored as a TransitionRemainder and added in on day 2.
02-assent moves aware people to sympathetic, driven by influence and
institutional support and slowed by policy coercion. 03-public moves
sympathetic people to public conformity, driven only by institutional
enforcement and policy coercion, and both start at 0. Rules run in rule-ID
order, each rule visits its cohorts in cohort-ID order, and each rule sees the
moves of the rules before it. On day 1, 02-assent therefore acts on the 118
newly aware villagers and moves 5 of them.
3. Let the council choose a policy
Section titled “3. Let the council choose a policy”register_policy_decision binds the controller council-controller to the
commander’s authority, with the government as command subject. It opens a
ticket with institutional_policy_ticket, which turns each PolicyChoice into
a decision option that carries a set_institutional_policy command:
PolicyChoice { id: "public-conformity".to_owned(), label: "Require public conformity".to_owned(), decision: PolicyDecision { alignment_id: "council-alignment".to_owned(), decision_version: 1, support_per_mille: 300, enforcement_per_mille: 800, access_grant_per_mille: 700, }, utility_inputs: BTreeMap::from([("control".to_owned(), 100)]),},The other choice, limited-access, has enforcement_per_mille: 0 and a
control input of 10. main scores the ticket with a WeightedUtilityPolicy
that weights control by 1, so public-conformity wins 100 to 10. The ticket
and controller mechanics are the same as in
A warlord asks a neighbor for military aid.
The set_institutional_policy handler in
plugin.rs
accepts the command only from the ticket’s controller, for the institution
that owns the alignment (InstitutionalAlignment), on behalf of the
alignment’s authorized_actor, and with a decision_version newer than the
applied and pending ones. It stores
the decision as a pending plugin component, and main confirms that the
distributions have not moved:
let immediately_after_decision = load_society_state(&canwu)?;assert_eq!( immediately_after_decision.distributions, before_policy.distributions, "the policy choice must not instantly rewrite population dispositions");4. Apply the policy at the next Daily boundary
Section titled “4. Apply the policy at the next Daily boundary”settle_day settles a boundary with SystemCadence::Daily, which runs the
society plugin’s Daily systems:
- Phase 7,
settle-social-transitions:apply_pending_policiescopies the pending decision ontocouncil-alignment, then the transition rules run. - Phase 10,
evaluate-mobilization-candidates: none in this run. - Phase 12,
aggregate-social-state: the counts thatmainprints. - Phase 13,
materialize-society-projections: one estimate per observer.
With enforcement at 800 per mille, 03-public runs at
800 × 300,000 / 1,000 = 240,000 per million, and 5 of the 23 sympathetic
villagers become publicly conforming.
5. Read the observer’s estimate
Section titled “5. Read the observer’s estimate”let viewer = canwu.viewer_context(ids.observer)?;let projection = projection_for_viewer(&canwu, &viewer)?;projection_for_viewer checks that the ViewerContext equals the one Canwu
issues for that actor, then returns the SocietyProjection materialized for
the actor in phase 13. It reads only materialized projections, and an actor
without one gets an InvalidAuthority error.
The observer’s ObserverProfile detects public alignment at 800 per mille and
reports false positives at 10 per mille. Each estimate is
(true count × detection + everyone else × false positive rate) / 1,000,
rounded down:
- public:
(5 × 800 + 1,995 × 10) / 1,000 = 23 - tied: the network’s concealment of 500 per mille halves the private
detection rate of 200 to 100, so
(0 × 100 + 2,000 × 10) / 1,000 = 20
False positives across 1,995 villagers outweigh the 5 real conformers, so the observer overestimates.
6. Restore, fork, and replay
Section titled “6. Restore, fork, and replay”let saved = canwu.snapshot_json()?;let restored = from_society_snapshot_json(&saved)?;assert_eq!(restored.snapshot(), canwu.snapshot());let forked = canwu.fork();assert_eq!(forked.snapshot(), canwu.snapshot());let replayed = Canwu::replay_from_journal(&[&plugin], &canwu.replay_journal())?;assert_eq!(replayed.snapshot(), canwu.snapshot());After the engine’s snapshot checks, from_society_snapshot_json runs
validate_society_runtime. It checks that the society record’s core
references match the entities its payload names, recomputes the stored
aggregates, mobilization candidates, and projections to compare them, and
re-derives any queued society ingress from the ingress journal. Remainders are
part of the record, so a restored run carries the same fractions forward.
What to notice
Section titled “What to notice”- A policy decision changes an alignment’s signals at the next Daily boundary. People then move only through transition rules, so your rule weights decide how enforcement affects public conformity and private assent.
- Fractions of a person are kept in
TransitionRemainder, saved with the state, and added in at the next boundary. - The observer reads a projection with both missed and false detections. That estimate is what a client shows the player.
Try changing
Section titled “Try changing”- Policy choice. In
register_policy_decision, raise thecontrolinput oflimited-accessto 200. The utility policy selectslimited-access, whose enforcement is 0, so03-publichas a rate of 0 and day 2 printspublic=0. - Observer accuracy. Set
false_positive_per_mille: 0in the observer profile. The estimate becomespublic=4 tied=0, because5 × 800 / 1,000rounds down to 4. - Influence. Set
active: falseon thenetwork-contactedge. Only the market’s 128 per mille of influence remains, and day 1 printsaware=51. - More days. Call
settle_day(&mut canwu, 3)before reading the projection and watch how the counts and the estimate change on a third day.
Beyond the example
Section titled “Beyond the example”Mobilization candidates
Section titled “Mobilization candidates”When a bucket reaches mobilization: MobilizationBand::Active, the phase-10
system records a MobilizationCandidate for that distribution with the
mobilized headcount, the target’s organization capacity, and the policy
coercion on the cohort. Your political or security extension decides whether a
candidate becomes a protest, a riot, a migration, or something else.
Other features
Section titled “Other features”- Policy provenance. A
PolicyPressurecan record itsissuerand thedecision_versionof the decision that produced it, asgap_g31_society_policy_pressure_provenancein the rebase and policy provenance tests shows. - Cohort headcount rebase. This ingress (
cohort_headcount_rebase_v1) resets a cohort’s headcount to a cited population record owned by another plugin and rescales its distributions at the next Daily boundary, asgap_g32_society_cohort_rebasein the same tests shows. - Culture lifecycle.
canwu-cultureinstalls culture targets into society state and moves them betweenActive,Dormant, andRetired; see Society, culture, and law.
Source
Section titled “Source”Open the runnable example
Read the society framework tests
Read the rebase and policy provenance tests