Cost Model & Offer Selection

Overview

Provisioning now uses a deterministic cost model so planners can justify every choice. Each offer reports its base cost (operation type) and a proximity modifier derived from the graph structure. Selection sorts by the sum of those two values and breaks ties with the provisioner id, making replay stable and debuggable.

Base Costs

Operation

Base Cost

Description

DIRECT

10

Reuse an existing node without modifications.

LIGHT_INDIRECT

50

Update an existing node in place.

HEAVY_INDIRECT

100

Clone and evolve an existing node.

CREATE

200

Instantiate a new node from a template.

These values map to :class:~tangl.vm.provision.offer.ProvisionCost and live on :class:~tangl.vm.provision.offer.ProvisionOffer.base_cost.

Proximity Modifiers

Graph distance influences the final score for EXISTING offers:

Relationship

Modifier

Example

Same block

+0

Node already attached to the requesting block.

Same scene

+5

Node and requester share the same immediate parent subgraph.

Same episode

+10

Node lives elsewhere in the current episode.

Elsewhere

+20

Node is outside the current episode hierarchy.

These values are added on top of the base cost; the sum is stored on offer.cost. Template provisioning does not apply a proximity modifier because it always creates a new instance.

Selection Algorithm

  1. Collect offers from all provisioners and attach metadata (provisioner id, layer, selection criteria).

  2. Deduplicate EXISTING offers per provider so the cheapest proposal survives.

  3. Sort the remaining offers by (cost, proximity, registration order).

  4. Record metadata (reason, all offers, selected provider) for the requirement.

  5. Accept the winning offer and emit :class:~tangl.vm.provision.offer.BuildReceipt plus a selection_audit entry in :class:~tangl.vm.provision.offer.PlanningReceipt.

Because template creation costs 200 and distant reuse tops out at 10 + 20 = 30, existing entities will always win unless the requirement policy is explicitly CREATE.

Debugging & Auditing

Use :class:tangl.vm.debug.PlanningDebugger to print audit data during development:

from tangl.vm.debug import PlanningDebugger

receipt = frame.records[-1]  # last PlanningReceipt emitted by the frame
PlanningDebugger.print_receipt(receipt)

Each entry lists all offers, their costs, proximity descriptions, and which provisioner won. Developers can also call PlanningDebugger.compare_offers(offers) to compare raw offers before the planner runs.

Troubleshooting

  • “Why did it reuse a distant node?” – Existing nodes max out at 30, so they beat template creation unless the requirement policy is CREATE or no existing offers qualify.

  • “Why was my template ignored?” – Templates only run when the requirement includes template_ref or template. GraphProvisioner skips those requests.

  • “How do I force a fresh instance?” – Set requirement_policy: CREATE in the script. The cost model still records the offer but GraphProvisioner will decline to compete.

  • “The audit trail is empty.” – Ensure planning handlers returned a :class:~tangl.vm.provision.offer.PlanningReceipt. Off-main-thread tests may need to call :func:tangl.vm.dispatch.planning.plan_collect_offers / plan_select_and_apply to populate it.