Skip to content

Planning: the machine-readable dry-run contract

Harness.preview() answers "what would an apply do, and why" as stable structured data — for CI gates, deployment tooling, and operator review — computed with zero mutation (guarantee G26). It runs the full pipeline a subsequent apply_config() + reconcile() would: parsing, migration, validation, catalog resolution, desired-state diffing, dependency resolution, semantic-identity reuse analysis, and generation-impact prediction.

plan = harness.preview()                      # the next reconcile
plan = harness.preview(open("harness.yaml"))  # a configuration change

plan.would_publish          # would this apply publish a new generation?
plan.to_dict()              # deterministic, versioned JSON document
plan.action_for("memory")   # one entry's planned action

Preview performs no mutation: no revision bump, no dirty flag, no mounted plugin, no published generation, and no setup or cleanup effect. Plugin objects are constructed exactly as install() would (so instance-configured manifests work) and discarded unmounted.

Actions

One action per affected entry, ordered by entry id, with a final publication action last. The vocabulary is closed (chassis.planning.ActionKind):

Action Meaning
add the entry joins the composition with a new instance
remove the entry leaves the composition
replace desired state replaces the entry (implementation or configuration changed)
reuse the exact mounted instance is carried into the next generation
rebuild the entry stays, but a semantic input changed so it must be rebuilt
reject the entry cannot join: unsatisfied, ambiguous, pending, or cyclic
publish the apply would publish a new generation
no-op the composition is identical; nothing would be published

Every action carries stable reason codes (ReasonCode) — the values are the contract, never rendered prose — plus, where applicable: the affected entry and scope, configuration keys (never values), the dependency or capability cause (cause_capability / cause_provider), expected_reuse with the expected instance_id, generation_impact, machine-readable validation_failures (code + detail), and ambiguities (candidates and the applicable preference).

{
  "action": "rebuild",
  "reasons": ["dependency_changed"],
  "entry_id": "memory",
  "plugin": "memory@2.1.0",
  "scope": "/",
  "config_keys": [],
  "cause_capability": "database",
  "cause_provider": "orders-db",
  "expected_reuse": false,
  "instance_id": null,
  "generation_impact": "new_generation",
  "validation_failures": [],
  "ambiguities": []
}

The document

PlanResult.to_dict() is deterministic and declares its own format version (PLAN_FORMAT_VERSION, never the package version — see compatibility.md):

{
  "format_version": 1,
  "chassis_version": "0.9.0",
  "would_publish": true,
  "current_generation_id": "gen_0007",
  "pending": ["memory"],
  "cycles": [],
  "preferences": {"memory:database": "orders-db"},
  "actions": ["…"]
}

preferences are the effective provider preferences the plan resolved with — selector keys to provider entry ids, never configuration material. Sensitive configuration appears only as config_keys; a preview output contains no configuration values at all.

What preview does and does not decide

Preview reports plan-level problems as reject actions with structured failures: unsatisfied or ambiguous requirements (no_provider, version_mismatch, not_visible, self_reference, ambiguous), provider-pending dependencies, and dependency cycles. Configuration-level errors (an unknown plugin, a malformed preference) raise exactly what apply_config() would raise. Publication-time contract validation — checking that mounted instances actually register what their manifests promise — needs mounted registrations and runs only during a real reconcile().

Parity with apply is a tested contract: the previewed actions are compared with what apply_config() + reconcile() actually mounted, reused, and disposed, and with the generation-impact analysis it reports (tests/planning/test_preview_parity.py).

There is deliberately no CLI: the contract is a library API. Tooling should call Harness.preview() and read to_dict(); a thin wrapper script would add no semantics of its own.