Add ActionReason to ResourceChange

Terraform emits an action_reason on each resource change in the JSON plan
format, explaining why the planned actions were chosen — most notably
"replace_because_tainted" when a resource is being replaced because its prior
object was tainted. The library did not expose this field, so consumers had no
way to distinguish, for example, a tainted replacement from any other
replacement.

This adds a typed ActionReason field (with constants for the documented values)
and a test fixture covering a tainted replacement.
diff --git a/plan.go b/plan.go
index 8875200..3b036c1 100644
--- a/plan.go
+++ b/plan.go
@@ -208,8 +208,81 @@
 
 	// The data describing the change that will be made to this object.
 	Change *Change `json:"change,omitempty"`
+
+	// ActionReason is an optional keyword providing extra context for why the
+	// actions in Change.Actions were chosen, for example that a resource must
+	// be replaced because its existing object was tainted. It is empty when
+	// Terraform did not report a reason. This detail is for display purposes
+	// only and should not be used to make decisions.
+	ActionReason ActionReason `json:"action_reason,omitempty"`
 }
 
+// ActionReason is a keyword representing the optional reason Terraform reports
+// for the actions proposed in a ResourceChange. The set of possible values may
+// grow in future Terraform versions, so consumers should treat unrecognized
+// values as equivalent to ActionReasonNone.
+type ActionReason string
+
+const (
+	// ActionReasonNone is the absence of any specific reason; Terraform omits
+	// the action_reason property in this case.
+	ActionReasonNone ActionReason = ""
+
+	// ActionReasonReplaceBecauseCannotUpdate indicates a resource must be
+	// replaced because the requested change is not possible without doing so.
+	ActionReasonReplaceBecauseCannotUpdate ActionReason = "replace_because_cannot_update"
+
+	// ActionReasonReplaceBecauseTainted indicates a resource must be replaced
+	// because its existing object was marked as tainted.
+	ActionReasonReplaceBecauseTainted ActionReason = "replace_because_tainted"
+
+	// ActionReasonReplaceByRequest indicates a resource must be replaced because
+	// the user explicitly requested it (e.g. "terraform plan -replace=...").
+	ActionReasonReplaceByRequest ActionReason = "replace_by_request"
+
+	// ActionReasonReplaceByTriggers indicates a resource must be replaced
+	// because of a change in a value referenced by its replace_triggered_by.
+	ActionReasonReplaceByTriggers ActionReason = "replace_by_triggers"
+
+	// ActionReasonDeleteBecauseNoResourceConfig indicates a resource will be
+	// destroyed because it has no matching configuration block.
+	ActionReasonDeleteBecauseNoResourceConfig ActionReason = "delete_because_no_resource_config"
+
+	// ActionReasonDeleteBecauseWrongRepetition indicates a resource instance
+	// will be destroyed because its instance key does not match the resource's
+	// count/for_each repetition mode.
+	ActionReasonDeleteBecauseWrongRepetition ActionReason = "delete_because_wrong_repetition"
+
+	// ActionReasonDeleteBecauseCountIndex indicates a resource instance will be
+	// destroyed because its index is out of range for the count value.
+	ActionReasonDeleteBecauseCountIndex ActionReason = "delete_because_count_index"
+
+	// ActionReasonDeleteBecauseEachKey indicates a resource instance will be
+	// destroyed because its key is not present in the for_each value.
+	ActionReasonDeleteBecauseEachKey ActionReason = "delete_because_each_key"
+
+	// ActionReasonDeleteBecauseNoModule indicates a resource instance will be
+	// destroyed because its containing module instance is no longer declared.
+	ActionReasonDeleteBecauseNoModule ActionReason = "delete_because_no_module"
+
+	// ActionReasonDeleteBecauseNoMoveTarget indicates a resource was moved to an
+	// address that has no configuration, so it will be destroyed.
+	ActionReasonDeleteBecauseNoMoveTarget ActionReason = "delete_because_no_move_target"
+
+	// ActionReasonReadBecauseConfigUnknown indicates a data resource will be
+	// read during apply because its configuration contains unknown values.
+	ActionReasonReadBecauseConfigUnknown ActionReason = "read_because_config_unknown"
+
+	// ActionReasonReadBecauseDependencyPending indicates a data resource will be
+	// read during apply because it depends on a resource or module with changes
+	// pending.
+	ActionReasonReadBecauseDependencyPending ActionReason = "read_because_dependency_pending"
+
+	// ActionReasonReadBecauseCheckNested indicates a data resource nested within
+	// a check block will be read during apply.
+	ActionReasonReadBecauseCheckNested ActionReason = "read_because_check_nested"
+)
+
 // Change is the representation of a proposed change for an object.
 type Change struct {
 	// The action to be carried out by this change.
diff --git a/plan_test.go b/plan_test.go
index 2235678..e38b6d6 100644
--- a/plan_test.go
+++ b/plan_test.go
@@ -137,6 +137,27 @@
 	}
 }
 
+func TestPlan_actionReason(t *testing.T) {
+	f, err := os.Open("testdata/action_reason/plan.json")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer f.Close()
+
+	var plan *Plan
+	if err := json.NewDecoder(f).Decode(&plan); err != nil {
+		t.Fatal(err)
+	}
+
+	if err := plan.Validate(); err != nil {
+		t.Fatal(err)
+	}
+
+	if got, want := plan.ResourceChanges[0].ActionReason, ActionReasonReplaceBecauseTainted; got != want {
+		t.Fatalf("unexpected action reason: got %q, want %q", got, want)
+	}
+}
+
 func TestPlan_actionInvocations(t *testing.T) {
 	f, err := os.Open("testdata/actions/plan.json")
 	if err != nil {
diff --git a/testdata/action_reason/plan.json b/testdata/action_reason/plan.json
new file mode 100644
index 0000000..1b1ad14
--- /dev/null
+++ b/testdata/action_reason/plan.json
@@ -0,0 +1 @@
+{"format_version":"1.2","terraform_version":"1.15.0","planned_values":{"root_module":{"resources":[{"address":"null_resource.example","mode":"managed","type":"null_resource","name":"example","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"triggers":{"name":"demo"}},"sensitive_values":{"triggers":{}}}]}},"resource_changes":[{"address":"null_resource.example","mode":"managed","type":"null_resource","name":"example","provider_name":"registry.terraform.io/hashicorp/null","change":{"actions":["delete","create"],"before":{"id":"8868159889619753631","triggers":{"name":"demo"}},"after":{"triggers":{"name":"demo"}},"after_unknown":{"id":true,"triggers":{}},"before_sensitive":{"triggers":{}},"after_sensitive":{"triggers":{}}},"action_reason":"replace_because_tainted"}],"complete":true,"prior_state":{"format_version":"1.0","terraform_version":"1.15.0","values":{"root_module":{"resources":[{"address":"null_resource.example","mode":"managed","type":"null_resource","name":"example","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"id":"8868159889619753631","triggers":{"name":"demo"}},"sensitive_values":{"triggers":{}},"tainted":true}]}}},"configuration":{"provider_config":{"null":{"name":"null","full_name":"registry.terraform.io/hashicorp/null","version_constraint":"~\u003e 3.0"}},"root_module":{"resources":[{"address":"null_resource.example","mode":"managed","type":"null_resource","name":"example","provider_config_key":"null","expressions":{"triggers":{"constant_value":{"name":"demo"}}},"schema_version":0}]}},"timestamp":"2026-06-16T10:21:47Z"}