Skip to content

Intents and the resolver

IntentResolver

Kind Engine core — the only writer of the graph
Project NextIteration.Core/Intents/
Exposes the Intent records; ResolveAndApply(graph, batch)
Consumes one batch of intents per phase
Produces graph writes; intent_applied / intent_rejected / intent_conflict events; the change set
Status designed · Phase 1 (#28)
PLAN §4, §11.1, §18.1, §18.5, §18.6, §18.8
Player Water and buckets (the bucket race)

Systems never mutate the graph. They yield intents — proposed changes — and the resolver validates, resolves conflicts, applies, clamps and records them. This indirection is what gives the engine conflict resolution, determinism, and a complete audit of what was attempted.

Intent types

abstract record Intent(int Priority, int SystemOrder, string SourceSystem)
{
    IReadOnlyDictionary<string, float>? Inputs { get; init; }   // optional: what this was computed from — for the trace (§13.5)
}
record SetAttributeIntent        // health = 50
record DeltaAttributeIntent      // temperature += 5
record AddEdgeIntent / RemoveEdgeIntent
record SetEdgePropertyIntent     // door.state = "locked"; a fact's value          (Phase 1)
record CreateNodeIntent / DestroyNodeIntent
record PropagationIntent(Source, Channel, Attribute, InitialIntensity, DeltaFactor, MinThreshold)   // engine expands to deltas (§11.1)

SourceSystem is system:<name>, rule:<id> or player:<id> — the same vocabulary the trace and the viewer use.

Resolution policy

All of a phase's intents are collected and resolved in one batch, grouped by target (node, key):

Target Strategy
Numeric attribute, deltas summed — deltas compose; two systems warming a room add up
State attribute or edge property, sets priority wins; equal priority → StableHash(tick, actorId, intentOrdinal) tiebreak (an input that differs between rivals), and an intent_conflict event
contains edges single-container rule: a node has exactly one container; several AddEdge(contains, X) in one batch → one winner, the rest rejected with intent_conflict. This is what makes a contested bucket resolve without any system knowing there was a contest, and why water lives in the bucket.
Create / destroy validated for dependencies, first valid wins; a destroyed node's contents must be handled (dropped)
PropagationIntent BFS over edges with permeability.<channel>, emitting deltas attributed to the requesting system; reach bounded by permeability × MinThreshold, never a hop cap

Within the batch, application is sequential in resolution order (fifty attacks land one after another).

Bounds

After applying, values are clamped to the world schema's bounds (§18.5, §18.8): soft bounds for system deltas, hard bounds for everything. Every depletable attribute must declare a min — a schema entry for food_value, water_level, fuel, wood_count without one is rejected at load, because eight NPCs reading the same frozen food_value = 2 and each taking a bite would otherwise sum to −2.

What the resolver leaves behind

  • The applied changes, each with a new Generation.
  • The per-tick change set (node, attribute, generation) — read by rules (Phase 3) and the trace.
  • Events: intent_applied, intent_rejected { reason }, intent_conflict { rival }, plus whatever the intent's semantics emit (item_taken, ignition …).
  • The trace's intent records, in resolution order, with ordinals (see Causal trace).

History

Phase Change
1 Introduced with per-phase batching, the single-container rule, a tiebreak that differs between rivals, required lower bounds, SetEdgePropertyIntent, optional Inputs.

See also

The graph · The tick · Causal trace · Determinism